custom-fields.e2e-spec.ts 53 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508
  1. import { LanguageCode } from '@vendure/common/lib/generated-types';
  2. import {
  3. Asset,
  4. CustomFields,
  5. Logger,
  6. mergeConfig,
  7. OrderService,
  8. ProductService,
  9. RequestContextService,
  10. TransactionalConnection,
  11. } from '@vendure/core';
  12. import { createErrorResultGuard, createTestEnvironment, ErrorResultGuard } from '@vendure/testing';
  13. import { fail } from 'node:assert';
  14. import path from 'node:path';
  15. import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest';
  16. import { initialData } from '../../../e2e-common/e2e-initial-data';
  17. import { TEST_SETUP_TIMEOUT_MS, testConfig } from '../../../e2e-common/test-config';
  18. import { graphql, ResultOf } from './graphql/graphql-admin';
  19. import { graphql as graphqlShop, ResultOf as ResultOfShop } from './graphql/graphql-shop';
  20. import { assertThrowsWithMessage } from './utils/assert-throws-with-message';
  21. import { fixPostgresTimezone } from './utils/fix-pg-timezone';
  22. fixPostgresTimezone();
  23. const validateInjectorSpy = vi.fn();
  24. const customConfig = mergeConfig(testConfig(), {
  25. dbConnectionOptions: {
  26. timezone: 'Z',
  27. },
  28. customFields: {
  29. Product: [
  30. { name: 'nullable', type: 'string' },
  31. { name: 'notNullable', type: 'string', nullable: false, defaultValue: '' },
  32. { name: 'stringWithDefault', type: 'string', defaultValue: 'hello' },
  33. { name: 'localeStringWithDefault', type: 'localeString', defaultValue: 'hola' },
  34. { name: 'intWithDefault', type: 'int', defaultValue: 5 },
  35. { name: 'floatWithDefault', type: 'float', defaultValue: 5.5678 },
  36. { name: 'booleanWithDefault', type: 'boolean', defaultValue: true },
  37. {
  38. name: 'dateTimeWithDefault',
  39. type: 'datetime',
  40. defaultValue: new Date('2019-04-30T12:59:16.4158386Z'),
  41. },
  42. { name: 'validateString', type: 'string', pattern: '^[0-9][a-z]+$' },
  43. { name: 'validateLocaleString', type: 'localeString', pattern: '^[0-9][a-z]+$' },
  44. { name: 'validateInt', type: 'int', min: 0, max: 10 },
  45. { name: 'validateFloat', type: 'float', min: 0.5, max: 10.5 },
  46. {
  47. name: 'validateDateTime',
  48. type: 'datetime',
  49. min: '2019-01-01T08:30',
  50. max: '2019-06-01T08:30',
  51. },
  52. {
  53. name: 'validateFn1',
  54. type: 'string',
  55. validate: value => {
  56. if (value !== 'valid') {
  57. return `The value ['${value as string}'] is not valid`;
  58. }
  59. },
  60. },
  61. {
  62. name: 'validateFn2',
  63. type: 'string',
  64. validate: value => {
  65. if (value !== 'valid') {
  66. return [
  67. {
  68. languageCode: LanguageCode.en,
  69. value: `The value ['${value as string}'] is not valid`,
  70. },
  71. ];
  72. }
  73. },
  74. },
  75. {
  76. name: 'validateFn3',
  77. type: 'string',
  78. validate: (value, injector) => {
  79. const connection = injector.get(TransactionalConnection);
  80. validateInjectorSpy(connection);
  81. },
  82. },
  83. {
  84. name: 'validateFn4',
  85. type: 'string',
  86. validate: async (value, injector) => {
  87. await new Promise(resolve => setTimeout(resolve, 1));
  88. return 'async error';
  89. },
  90. },
  91. {
  92. name: 'validateRelation',
  93. type: 'relation',
  94. entity: Asset,
  95. validate: async value => {
  96. await new Promise(resolve => setTimeout(resolve, 1));
  97. return 'relation error';
  98. },
  99. },
  100. {
  101. name: 'stringWithOptions',
  102. type: 'string',
  103. options: [{ value: 'small' }, { value: 'medium' }, { value: 'large' }],
  104. },
  105. {
  106. name: 'nullableStringWithOptions',
  107. type: 'string',
  108. nullable: true,
  109. options: [{ value: 'small' }, { value: 'medium' }, { value: 'large' }],
  110. },
  111. {
  112. name: 'nonPublic',
  113. type: 'string',
  114. defaultValue: 'hi!',
  115. public: false,
  116. },
  117. {
  118. name: 'public',
  119. type: 'string',
  120. defaultValue: 'ho!',
  121. public: true,
  122. },
  123. {
  124. name: 'longString',
  125. type: 'string',
  126. length: 10000,
  127. },
  128. {
  129. name: 'longLocaleString',
  130. type: 'localeString',
  131. length: 10000,
  132. },
  133. {
  134. name: 'readonlyString',
  135. type: 'string',
  136. readonly: true,
  137. },
  138. {
  139. name: 'internalString',
  140. type: 'string',
  141. internal: true,
  142. },
  143. {
  144. name: 'stringList',
  145. type: 'string',
  146. list: true,
  147. },
  148. {
  149. name: 'localeStringList',
  150. type: 'localeString',
  151. list: true,
  152. },
  153. {
  154. name: 'stringListWithDefault',
  155. type: 'string',
  156. list: true,
  157. defaultValue: ['cat'],
  158. },
  159. {
  160. name: 'intListWithValidation',
  161. type: 'int',
  162. list: true,
  163. validate: value => {
  164. if (!value.includes(42)) {
  165. return 'Must include the number 42!';
  166. }
  167. },
  168. },
  169. {
  170. name: 'uniqueString',
  171. type: 'string',
  172. unique: true,
  173. },
  174. ],
  175. Facet: [
  176. {
  177. name: 'translated',
  178. type: 'localeString',
  179. },
  180. ],
  181. Customer: [
  182. {
  183. name: 'score',
  184. type: 'int',
  185. readonly: true,
  186. },
  187. ],
  188. Collection: [
  189. { name: 'secretKey1', type: 'string', defaultValue: '', public: false, internal: true },
  190. { name: 'secretKey2', type: 'string', defaultValue: '', public: false, internal: false },
  191. ],
  192. OrderLine: [{ name: 'validateInt', type: 'int', min: 0, max: 10 }],
  193. ProductVariantPrice: [
  194. {
  195. name: 'costPrice',
  196. type: 'int',
  197. },
  198. ],
  199. // Single readonly Address custom field to test
  200. // https://github.com/vendure-ecommerce/vendure/issues/3326
  201. Address: [
  202. {
  203. name: 'hereId',
  204. type: 'string',
  205. readonly: true,
  206. nullable: true,
  207. },
  208. ],
  209. } as CustomFields,
  210. });
  211. describe('Custom fields', () => {
  212. const { server, adminClient, shopClient } = createTestEnvironment(customConfig);
  213. // Product guard for nullable results with customFields
  214. type ProductWithCustomFields = NonNullable<ResultOf<typeof getProductNullableDocument>['product']>;
  215. const productGuard: ErrorResultGuard<ProductWithCustomFields> = createErrorResultGuard(
  216. input => input !== null && 'customFields' in input,
  217. );
  218. // Collection guard for shop API
  219. type CollectionResult = NonNullable<ResultOfShop<typeof getCollectionCustomFieldsDocument>['collection']>;
  220. const collectionGuard: ErrorResultGuard<CollectionResult> = createErrorResultGuard(
  221. input => input !== null,
  222. );
  223. // ProductVariant guard (custom document with price customFields)
  224. type ProductVariantResult = ResultOf<
  225. typeof updateProductVariantsWithPriceCustomFieldsDocument
  226. >['updateProductVariants'][number];
  227. const productVariantGuard: ErrorResultGuard<ProductVariantResult> = createErrorResultGuard(
  228. input => input !== null && 'prices' in input,
  229. );
  230. // Order guard for shop API validation tests
  231. type OrderWithLines = { id: string; lines: any[] };
  232. const orderGuard: ErrorResultGuard<OrderWithLines> = createErrorResultGuard(input => 'lines' in input);
  233. beforeAll(async () => {
  234. await server.init({
  235. initialData,
  236. productsCsvPath: path.join(__dirname, 'fixtures/e2e-products-minimal.csv'),
  237. customerCount: 1,
  238. });
  239. await adminClient.asSuperAdmin();
  240. }, TEST_SETUP_TIMEOUT_MS);
  241. afterAll(async () => {
  242. await server.destroy();
  243. });
  244. it('globalSettings.serverConfig.customFieldConfig', async () => {
  245. const { globalSettings } = await adminClient.query(getServerConfigCustomFieldsDocument);
  246. expect(globalSettings.serverConfig.customFieldConfig).toEqual({
  247. Product: [
  248. { name: 'nullable', type: 'string', list: false },
  249. { name: 'notNullable', type: 'string', list: false },
  250. { name: 'stringWithDefault', type: 'string', list: false },
  251. { name: 'localeStringWithDefault', type: 'localeString', list: false },
  252. { name: 'intWithDefault', type: 'int', list: false },
  253. { name: 'floatWithDefault', type: 'float', list: false },
  254. { name: 'booleanWithDefault', type: 'boolean', list: false },
  255. { name: 'dateTimeWithDefault', type: 'datetime', list: false },
  256. { name: 'validateString', type: 'string', list: false },
  257. { name: 'validateLocaleString', type: 'localeString', list: false },
  258. { name: 'validateInt', type: 'int', list: false },
  259. { name: 'validateFloat', type: 'float', list: false },
  260. { name: 'validateDateTime', type: 'datetime', list: false },
  261. { name: 'validateFn1', type: 'string', list: false },
  262. { name: 'validateFn2', type: 'string', list: false },
  263. { name: 'validateFn3', type: 'string', list: false },
  264. { name: 'validateFn4', type: 'string', list: false },
  265. {
  266. name: 'validateRelation',
  267. type: 'relation',
  268. list: false,
  269. scalarFields: [
  270. 'id',
  271. 'createdAt',
  272. 'updatedAt',
  273. 'name',
  274. 'type',
  275. 'fileSize',
  276. 'mimeType',
  277. 'width',
  278. 'height',
  279. 'source',
  280. 'preview',
  281. 'customFields',
  282. ],
  283. },
  284. { name: 'stringWithOptions', type: 'string', list: false },
  285. { name: 'nullableStringWithOptions', type: 'string', list: false },
  286. { name: 'nonPublic', type: 'string', list: false },
  287. { name: 'public', type: 'string', list: false },
  288. { name: 'longString', type: 'string', list: false },
  289. { name: 'longLocaleString', type: 'localeString', list: false },
  290. { name: 'readonlyString', type: 'string', list: false },
  291. { name: 'stringList', type: 'string', list: true },
  292. { name: 'localeStringList', type: 'localeString', list: true },
  293. { name: 'stringListWithDefault', type: 'string', list: true },
  294. { name: 'intListWithValidation', type: 'int', list: true },
  295. { name: 'uniqueString', type: 'string', list: false },
  296. // The internal type should not be exposed at all
  297. // { name: 'internalString', type: 'string' },
  298. ],
  299. });
  300. });
  301. it('globalSettings.serverConfig.entityCustomFields', async () => {
  302. const { globalSettings } = await adminClient.query(getServerConfigEntityCustomFieldsDocument);
  303. const productCustomFields = globalSettings.serverConfig.entityCustomFields.find(
  304. e => e.entityName === 'Product',
  305. );
  306. expect(productCustomFields).toEqual({
  307. entityName: 'Product',
  308. customFields: [
  309. { name: 'nullable', type: 'string', list: false },
  310. { name: 'notNullable', type: 'string', list: false },
  311. { name: 'stringWithDefault', type: 'string', list: false },
  312. { name: 'localeStringWithDefault', type: 'localeString', list: false },
  313. { name: 'intWithDefault', type: 'int', list: false },
  314. { name: 'floatWithDefault', type: 'float', list: false },
  315. { name: 'booleanWithDefault', type: 'boolean', list: false },
  316. { name: 'dateTimeWithDefault', type: 'datetime', list: false },
  317. { name: 'validateString', type: 'string', list: false },
  318. { name: 'validateLocaleString', type: 'localeString', list: false },
  319. { name: 'validateInt', type: 'int', list: false },
  320. { name: 'validateFloat', type: 'float', list: false },
  321. { name: 'validateDateTime', type: 'datetime', list: false },
  322. { name: 'validateFn1', type: 'string', list: false },
  323. { name: 'validateFn2', type: 'string', list: false },
  324. { name: 'validateFn3', type: 'string', list: false },
  325. { name: 'validateFn4', type: 'string', list: false },
  326. {
  327. name: 'validateRelation',
  328. type: 'relation',
  329. list: false,
  330. scalarFields: [
  331. 'id',
  332. 'createdAt',
  333. 'updatedAt',
  334. 'name',
  335. 'type',
  336. 'fileSize',
  337. 'mimeType',
  338. 'width',
  339. 'height',
  340. 'source',
  341. 'preview',
  342. 'customFields',
  343. ],
  344. },
  345. { name: 'stringWithOptions', type: 'string', list: false },
  346. { name: 'nullableStringWithOptions', type: 'string', list: false },
  347. { name: 'nonPublic', type: 'string', list: false },
  348. { name: 'public', type: 'string', list: false },
  349. { name: 'longString', type: 'string', list: false },
  350. { name: 'longLocaleString', type: 'localeString', list: false },
  351. { name: 'readonlyString', type: 'string', list: false },
  352. { name: 'stringList', type: 'string', list: true },
  353. { name: 'localeStringList', type: 'localeString', list: true },
  354. { name: 'stringListWithDefault', type: 'string', list: true },
  355. { name: 'intListWithValidation', type: 'int', list: true },
  356. { name: 'uniqueString', type: 'string', list: false },
  357. // The internal type should not be exposed at all
  358. // { name: 'internalString', type: 'string' },
  359. ],
  360. });
  361. });
  362. it('get nullable with no default', async () => {
  363. const { product } = await adminClient.query(getProductNullableDocument, { id: 'T_1' });
  364. expect(product).toEqual({
  365. id: 'T_1',
  366. name: 'Laptop',
  367. customFields: {
  368. nullable: null,
  369. },
  370. });
  371. });
  372. it('get entity with localeString only', async () => {
  373. const { facet } = await adminClient.query(getFacetCustomFieldsDocument, { id: 'T_1' });
  374. expect(facet).toEqual({
  375. id: 'T_1',
  376. name: 'category',
  377. customFields: {
  378. translated: null,
  379. },
  380. });
  381. });
  382. it('get fields with default values', async () => {
  383. const { product } = await adminClient.query(getProductWithDefaultsDocument, { id: 'T_1' });
  384. const customFields = {
  385. stringWithDefault: 'hello',
  386. localeStringWithDefault: 'hola',
  387. intWithDefault: 5,
  388. floatWithDefault: 5.5678,
  389. booleanWithDefault: true,
  390. dateTimeWithDefault: '2019-04-30T12:59:16.415Z',
  391. // MySQL does not support defaults on TEXT fields, which is what "simple-json" uses
  392. // internally. See https://stackoverflow.com/q/3466872/772859
  393. stringListWithDefault: customConfig.dbConnectionOptions.type === 'mysql' ? null : ['cat'],
  394. };
  395. expect(product).toEqual({
  396. id: 'T_1',
  397. name: 'Laptop',
  398. customFields,
  399. });
  400. });
  401. it(
  402. 'update non-nullable field',
  403. assertThrowsWithMessage(async () => {
  404. await adminClient.query(
  405. graphql(`
  406. mutation {
  407. updateProduct(input: { id: "T_1", customFields: { notNullable: null } }) {
  408. id
  409. }
  410. }
  411. `),
  412. );
  413. }, 'The custom field "notNullable" value cannot be set to null'),
  414. );
  415. it(
  416. 'throws on attempt to update readonly field',
  417. assertThrowsWithMessage(async () => {
  418. await adminClient.query(
  419. graphql(`
  420. mutation {
  421. updateProduct(input: { id: "T_1", customFields: { readonlyString: "hello" } }) {
  422. id
  423. }
  424. }
  425. `),
  426. );
  427. }, 'Field "readonlyString" is not defined by type "UpdateProductCustomFieldsInput"'),
  428. );
  429. it(
  430. 'throws on attempt to update readonly field when no other custom fields defined',
  431. assertThrowsWithMessage(async () => {
  432. await adminClient.query(
  433. graphql(`
  434. mutation {
  435. updateCustomer(input: { id: "T_1", customFields: { score: 5 } }) {
  436. ... on Customer {
  437. id
  438. }
  439. }
  440. }
  441. `),
  442. );
  443. }, 'The custom field "score" is readonly'),
  444. );
  445. it(
  446. 'throws on attempt to create readonly field',
  447. assertThrowsWithMessage(async () => {
  448. await adminClient.query(
  449. graphql(`
  450. mutation {
  451. createProduct(
  452. input: {
  453. translations: [{ languageCode: en, name: "test" }]
  454. customFields: { readonlyString: "hello" }
  455. }
  456. ) {
  457. id
  458. }
  459. }
  460. `),
  461. );
  462. }, 'Field "readonlyString" is not defined by type "CreateProductCustomFieldsInput"'),
  463. );
  464. it('string length allows long strings', async () => {
  465. const longString = Array.from({ length: 500 }, v => 'hello there!').join(' ');
  466. const result = await adminClient.query(updateProductLongStringDocument, {
  467. id: 'T_1',
  468. stringValue: longString,
  469. });
  470. expect(result.updateProduct.customFields.longString).toBe(longString);
  471. });
  472. it('string length allows long localeStrings', async () => {
  473. const longString = Array.from({ length: 500 }, v => 'hello there!').join(' ');
  474. const result = await adminClient.query(updateProductLongLocaleStringDocument, {
  475. id: 'T_1',
  476. stringValue: longString,
  477. });
  478. expect(result.updateProduct.customFields.longLocaleString).toBe(longString);
  479. });
  480. describe('validation', () => {
  481. it(
  482. 'invalid string',
  483. assertThrowsWithMessage(async () => {
  484. await adminClient.query(
  485. graphql(`
  486. mutation {
  487. updateProduct(input: { id: "T_1", customFields: { validateString: "hello" } }) {
  488. id
  489. }
  490. }
  491. `),
  492. );
  493. }, 'The custom field "validateString" value ["hello"] does not match the pattern [^[0-9][a-z]+$]'),
  494. );
  495. it(
  496. 'invalid string option',
  497. assertThrowsWithMessage(async () => {
  498. await adminClient.query(
  499. graphql(`
  500. mutation {
  501. updateProduct(input: { id: "T_1", customFields: { stringWithOptions: "tiny" } }) {
  502. id
  503. }
  504. }
  505. `),
  506. );
  507. }, "The custom field \"stringWithOptions\" value [\"tiny\"] is invalid. Valid options are ['small', 'medium', 'large']"),
  508. );
  509. it('valid string option', async () => {
  510. const { updateProduct } = await adminClient.query(updateProductStringOptionsDocument, {
  511. id: 'T_1',
  512. value: 'medium',
  513. });
  514. expect(updateProduct.customFields.stringWithOptions).toBe('medium');
  515. });
  516. it('nullable string option with null', async () => {
  517. const { updateProduct } = await adminClient.query(updateProductNullableStringOptionsDocument, {
  518. id: 'T_1',
  519. value: null,
  520. });
  521. expect(updateProduct.customFields.nullableStringWithOptions).toBeNull();
  522. });
  523. it(
  524. 'invalid localeString',
  525. assertThrowsWithMessage(async () => {
  526. await adminClient.query(
  527. graphql(`
  528. mutation {
  529. updateProduct(
  530. input: {
  531. id: "T_1"
  532. translations: [
  533. {
  534. id: "T_1"
  535. languageCode: en
  536. customFields: { validateLocaleString: "servus" }
  537. }
  538. ]
  539. }
  540. ) {
  541. id
  542. }
  543. }
  544. `),
  545. );
  546. }, 'The custom field "validateLocaleString" value ["servus"] does not match the pattern [^[0-9][a-z]+$]'),
  547. );
  548. it(
  549. 'invalid int',
  550. assertThrowsWithMessage(async () => {
  551. await adminClient.query(
  552. graphql(`
  553. mutation {
  554. updateProduct(input: { id: "T_1", customFields: { validateInt: 12 } }) {
  555. id
  556. }
  557. }
  558. `),
  559. );
  560. }, 'The custom field "validateInt" value [12] is greater than the maximum [10]'),
  561. );
  562. it(
  563. 'invalid float',
  564. assertThrowsWithMessage(async () => {
  565. await adminClient.query(
  566. graphql(`
  567. mutation {
  568. updateProduct(input: { id: "T_1", customFields: { validateFloat: 10.6 } }) {
  569. id
  570. }
  571. }
  572. `),
  573. );
  574. }, 'The custom field "validateFloat" value [10.6] is greater than the maximum [10.5]'),
  575. );
  576. it(
  577. 'invalid datetime',
  578. assertThrowsWithMessage(async () => {
  579. await adminClient.query(
  580. graphql(`
  581. mutation {
  582. updateProduct(
  583. input: {
  584. id: "T_1"
  585. customFields: { validateDateTime: "2019-01-01T05:25:00.000Z" }
  586. }
  587. ) {
  588. id
  589. }
  590. }
  591. `),
  592. );
  593. }, 'The custom field "validateDateTime" value [2019-01-01T05:25:00.000Z] is less than the minimum [2019-01-01T08:30]'),
  594. );
  595. it(
  596. 'invalid validate function with string',
  597. assertThrowsWithMessage(async () => {
  598. await adminClient.query(
  599. graphql(`
  600. mutation {
  601. updateProduct(input: { id: "T_1", customFields: { validateFn1: "invalid" } }) {
  602. id
  603. }
  604. }
  605. `),
  606. );
  607. }, "The value ['invalid'] is not valid"),
  608. );
  609. it(
  610. 'invalid validate function with localized string',
  611. assertThrowsWithMessage(async () => {
  612. await adminClient.query(
  613. graphql(`
  614. mutation {
  615. updateProduct(input: { id: "T_1", customFields: { validateFn2: "invalid" } }) {
  616. id
  617. }
  618. }
  619. `),
  620. );
  621. }, "The value ['invalid'] is not valid"),
  622. );
  623. it(
  624. 'invalid list field',
  625. assertThrowsWithMessage(async () => {
  626. await adminClient.query(
  627. graphql(`
  628. mutation {
  629. updateProduct(
  630. input: { id: "T_1", customFields: { intListWithValidation: [1, 2, 3] } }
  631. ) {
  632. id
  633. }
  634. }
  635. `),
  636. );
  637. }, 'Must include the number 42!'),
  638. );
  639. it('valid list field', async () => {
  640. const { updateProduct } = await adminClient.query(updateProductIntListDocument, {
  641. id: 'T_1',
  642. values: [1, 42, 3],
  643. });
  644. expect(updateProduct.customFields.intListWithValidation).toEqual([1, 42, 3]);
  645. });
  646. it('can inject providers into validation fn', async () => {
  647. const { updateProduct } = await adminClient.query(updateProductValidateFn3Document, {
  648. id: 'T_1',
  649. value: 'some value',
  650. });
  651. expect(updateProduct.customFields.validateFn3).toBe('some value');
  652. expect(validateInjectorSpy).toHaveBeenCalledTimes(1);
  653. expect(validateInjectorSpy.mock.calls[0][0] instanceof TransactionalConnection).toBe(true);
  654. });
  655. it(
  656. 'supports async validation fn',
  657. assertThrowsWithMessage(async () => {
  658. await adminClient.query(
  659. graphql(`
  660. mutation {
  661. updateProduct(input: { id: "T_1", customFields: { validateFn4: "some value" } }) {
  662. id
  663. customFields {
  664. validateFn4
  665. }
  666. }
  667. }
  668. `),
  669. );
  670. }, 'async error'),
  671. );
  672. // https://github.com/vendure-ecommerce/vendure/issues/1000
  673. it(
  674. 'supports validation of relation types',
  675. assertThrowsWithMessage(async () => {
  676. await adminClient.query(
  677. graphql(`
  678. mutation {
  679. updateProduct(input: { id: "T_1", customFields: { validateRelationId: "T_1" } }) {
  680. id
  681. customFields {
  682. validateFn4
  683. }
  684. }
  685. }
  686. `),
  687. );
  688. }, 'relation error'),
  689. );
  690. // https://github.com/vendure-ecommerce/vendure/issues/1091
  691. it('handles well graphql internal fields', async () => {
  692. // throws "Cannot read property 'args' of undefined" if broken
  693. await adminClient.query(
  694. graphql(`
  695. mutation {
  696. __typename
  697. updateProduct(input: { id: "T_1", customFields: { nullable: "some value" } }) {
  698. __typename
  699. id
  700. customFields {
  701. __typename
  702. nullable
  703. }
  704. }
  705. }
  706. `),
  707. );
  708. });
  709. // https://github.com/vendure-ecommerce/vendure/issues/1953
  710. describe('validation of OrderLine custom fields', () => {
  711. it('addItemToOrder', async () => {
  712. try {
  713. const { addItemToOrder } = await shopClient.query(
  714. graphqlShop(`
  715. mutation {
  716. addItemToOrder(
  717. productVariantId: 1
  718. quantity: 1
  719. customFields: { validateInt: 11 }
  720. ) {
  721. ... on Order {
  722. id
  723. }
  724. }
  725. }
  726. `),
  727. );
  728. fail('Should have thrown');
  729. } catch (e) {
  730. expect(e.message).toContain(
  731. 'The custom field "validateInt" value [11] is greater than the maximum [10]',
  732. );
  733. }
  734. const { addItemToOrder: result } = await shopClient.query(
  735. addItemToOrderWithCustomFieldsDocument,
  736. {
  737. productVariantId: '1',
  738. quantity: 1,
  739. validateInt: 9,
  740. },
  741. );
  742. orderGuard.assertSuccess(result);
  743. expect(result.lines[0].customFields).toEqual({ validateInt: 9 });
  744. });
  745. it('adjustOrderLine', async () => {
  746. try {
  747. const { adjustOrderLine } = await shopClient.query(
  748. graphqlShop(`
  749. mutation {
  750. adjustOrderLine(
  751. orderLineId: "T_1"
  752. quantity: 1
  753. customFields: { validateInt: 11 }
  754. ) {
  755. ... on Order {
  756. id
  757. }
  758. }
  759. }
  760. `),
  761. );
  762. fail('Should have thrown');
  763. } catch (e) {
  764. expect(e.message).toContain(
  765. 'The custom field "validateInt" value [11] is greater than the maximum [10]',
  766. );
  767. }
  768. const { adjustOrderLine: result } = await shopClient.query(
  769. adjustOrderLineWithCustomFieldsDocument,
  770. {
  771. orderLineId: 'T_1',
  772. quantity: 1,
  773. validateInt: 2,
  774. },
  775. );
  776. orderGuard.assertSuccess(result);
  777. expect(result.lines[0].customFields).toEqual({ validateInt: 2 });
  778. });
  779. });
  780. });
  781. describe('public access', () => {
  782. it(
  783. 'non-public throws for Shop API',
  784. assertThrowsWithMessage(async () => {
  785. await shopClient.query(
  786. graphqlShop(`
  787. query {
  788. product(id: "T_1") {
  789. id
  790. customFields {
  791. nonPublic
  792. }
  793. }
  794. }
  795. `),
  796. );
  797. }, 'Cannot query field "nonPublic" on type "ProductCustomFields"'),
  798. );
  799. it('publicly accessible via Shop API', async () => {
  800. const { product } = await shopClient.query(getShopProductPublicCustomFieldDocument, {
  801. id: 'T_1',
  802. });
  803. if (!product) {
  804. throw new Error('Product not found');
  805. }
  806. productGuard.assertSuccess(product);
  807. expect(product.customFields.public).toBe('ho!');
  808. });
  809. it(
  810. 'internal throws for Shop API',
  811. assertThrowsWithMessage(async () => {
  812. await shopClient.query(
  813. graphqlShop(`
  814. query {
  815. product(id: "T_1") {
  816. id
  817. customFields {
  818. internalString
  819. }
  820. }
  821. }
  822. `),
  823. );
  824. }, 'Cannot query field "internalString" on type "ProductCustomFields"'),
  825. );
  826. it(
  827. 'internal throws for Admin API',
  828. assertThrowsWithMessage(async () => {
  829. await adminClient.query(
  830. graphql(`
  831. query {
  832. product(id: "T_1") {
  833. id
  834. customFields {
  835. internalString
  836. }
  837. }
  838. }
  839. `),
  840. );
  841. }, 'Cannot query field "internalString" on type "ProductCustomFields"'),
  842. );
  843. // https://github.com/vendure-ecommerce/vendure/issues/3049
  844. it('does not leak private fields via JSON type', async () => {
  845. const { collection } = await shopClient.query(getCollectionCustomFieldsDocument, { id: 'T_1' });
  846. collectionGuard.assertSuccess(collection);
  847. expect(collection.customFields).toBe(null);
  848. });
  849. });
  850. describe('sort & filter', () => {
  851. it('can sort by custom fields', async () => {
  852. const { products } = await adminClient.query(getProductsSortByNullableDocument);
  853. expect(products.totalItems).toBe(1);
  854. });
  855. // https://github.com/vendure-ecommerce/vendure/issues/1581
  856. it('can sort by localeString custom fields', async () => {
  857. const { products } = await adminClient.query(getProductsSortByLocaleStringDocument);
  858. expect(products.totalItems).toBe(1);
  859. });
  860. it('can filter by custom fields', async () => {
  861. const { products } = await adminClient.query(getProductsFilterByStringDocument);
  862. expect(products.totalItems).toBe(1);
  863. });
  864. it('can filter by localeString custom fields', async () => {
  865. const { products } = await adminClient.query(getProductsFilterByLocaleStringDocument);
  866. expect(products.totalItems).toBe(1);
  867. });
  868. it('can filter by custom list fields', async () => {
  869. const { products: result1 } = await adminClient.query(getProductsFilterByIntListDocument, {
  870. value: 42,
  871. });
  872. expect(result1.totalItems).toBe(1);
  873. const { products: result2 } = await adminClient.query(getProductsFilterByIntListDocument, {
  874. value: 43,
  875. });
  876. expect(result2.totalItems).toBe(0);
  877. });
  878. it(
  879. 'cannot sort by custom list fields',
  880. assertThrowsWithMessage(async () => {
  881. await adminClient.query(
  882. graphql(`
  883. query {
  884. products(options: { sort: { intListWithValidation: ASC } }) {
  885. totalItems
  886. }
  887. }
  888. `),
  889. );
  890. }, 'Field "intListWithValidation" is not defined by type "ProductSortParameter".'),
  891. );
  892. it(
  893. 'cannot filter by internal field in Admin API',
  894. assertThrowsWithMessage(async () => {
  895. await adminClient.query(
  896. graphql(`
  897. query {
  898. products(options: { filter: { internalString: { contains: "hello" } } }) {
  899. totalItems
  900. }
  901. }
  902. `),
  903. );
  904. }, 'Field "internalString" is not defined by type "ProductFilterParameter"'),
  905. );
  906. it(
  907. 'cannot filter by internal field in Shop API',
  908. assertThrowsWithMessage(async () => {
  909. await shopClient.query(
  910. graphqlShop(`
  911. query {
  912. products(options: { filter: { internalString: { contains: "hello" } } }) {
  913. totalItems
  914. }
  915. }
  916. `),
  917. );
  918. }, 'Field "internalString" is not defined by type "ProductFilterParameter"'),
  919. );
  920. });
  921. describe('product on productVariant entity', () => {
  922. it('is translated', async () => {
  923. const { productVariants } = await adminClient.query(getProductVariantsWithCustomFieldsDocument, {
  924. productId: 'T_1',
  925. });
  926. expect(productVariants.items[0].product).toEqual({
  927. id: 'T_1',
  928. name: 'Laptop',
  929. customFields: {
  930. localeStringWithDefault: 'hola',
  931. stringWithDefault: 'hello',
  932. },
  933. });
  934. });
  935. });
  936. describe('unique constraint', () => {
  937. it('setting unique value works', async () => {
  938. const result = await adminClient.query(updateProductUniqueStringDocument, {
  939. id: 'T_1',
  940. value: 'foo',
  941. });
  942. expect(result.updateProduct.customFields.uniqueString).toBe('foo');
  943. });
  944. it('setting conflicting value fails', async () => {
  945. try {
  946. await adminClient.query(createProductUniqueStringDocument, { uniqueString: 'foo' });
  947. fail('Should have thrown');
  948. } catch (e: any) {
  949. let duplicateKeyErrMessage = 'unassigned';
  950. switch (customConfig.dbConnectionOptions.type) {
  951. case 'mariadb':
  952. case 'mysql':
  953. duplicateKeyErrMessage = "Duplicate entry 'foo' for key";
  954. break;
  955. case 'postgres':
  956. duplicateKeyErrMessage = 'duplicate key value violates unique constraint';
  957. break;
  958. case 'sqlite':
  959. case 'sqljs':
  960. duplicateKeyErrMessage = 'UNIQUE constraint failed: product.customFieldsUniquestring';
  961. break;
  962. }
  963. expect(e.message).toContain(duplicateKeyErrMessage);
  964. }
  965. });
  966. });
  967. it('on ProductVariantPrice', async () => {
  968. const { updateProductVariants } = await adminClient.query(
  969. updateProductVariantsWithPriceCustomFieldsDocument,
  970. {
  971. input: [
  972. {
  973. id: 'T_1',
  974. prices: [
  975. {
  976. price: 129900,
  977. currencyCode: 'USD',
  978. customFields: {
  979. costPrice: 100,
  980. },
  981. },
  982. ],
  983. },
  984. ],
  985. },
  986. );
  987. productVariantGuard.assertSuccess(updateProductVariants[0]);
  988. if (!updateProductVariants[0]) {
  989. throw new Error('Update product variants failed');
  990. }
  991. expect(updateProductVariants[0].prices).toEqual([
  992. {
  993. currencyCode: 'USD',
  994. price: 129900,
  995. customFields: {
  996. costPrice: 100,
  997. },
  998. },
  999. ]);
  1000. });
  1001. describe('setting custom fields directly via a service method', () => {
  1002. it('OrderService.addItemToOrder warns on unknown custom field', async () => {
  1003. const orderService = server.app.get(OrderService);
  1004. const requestContextService = server.app.get(RequestContextService);
  1005. const ctx = await requestContextService.create({
  1006. apiType: 'admin',
  1007. });
  1008. const order = await orderService.create(ctx);
  1009. const warnSpy = vi.spyOn(Logger, 'warn');
  1010. await orderService.addItemToOrder(ctx, order.id, 1, 1, {
  1011. customFieldWhichDoesNotExist: 'test value',
  1012. });
  1013. expect(warnSpy).toHaveBeenCalledWith(
  1014. 'Custom field customFieldWhichDoesNotExist not found for entity OrderLine',
  1015. );
  1016. });
  1017. it('OrderService.addItemToOrder does not warn on known custom field', async () => {
  1018. const orderService = server.app.get(OrderService);
  1019. const requestContextService = server.app.get(RequestContextService);
  1020. const ctx = await requestContextService.create({
  1021. apiType: 'admin',
  1022. });
  1023. const order = await orderService.create(ctx);
  1024. const warnSpy = vi.spyOn(Logger, 'warn');
  1025. await orderService.addItemToOrder(ctx, order.id, 1, 1, {
  1026. validateInt: 1,
  1027. });
  1028. expect(warnSpy).not.toHaveBeenCalled();
  1029. });
  1030. it('OrderService.addItemToOrder warns on multiple unknown custom fields', async () => {
  1031. const orderService = server.app.get(OrderService);
  1032. const requestContextService = server.app.get(RequestContextService);
  1033. const ctx = await requestContextService.create({
  1034. apiType: 'admin',
  1035. });
  1036. const order = await orderService.create(ctx);
  1037. const warnSpy = vi.spyOn(Logger, 'warn');
  1038. await orderService.addItemToOrder(ctx, order.id, 1, 1, {
  1039. unknownField1: 'foo',
  1040. unknownField2: 'bar',
  1041. });
  1042. expect(warnSpy).toHaveBeenCalledWith('Custom field unknownField1 not found for entity OrderLine');
  1043. expect(warnSpy).toHaveBeenCalledWith('Custom field unknownField2 not found for entity OrderLine');
  1044. });
  1045. it('OrderService.addItemToOrder does not warn when no custom fields are provided', async () => {
  1046. const orderService = server.app.get(OrderService);
  1047. const requestContextService = server.app.get(RequestContextService);
  1048. const ctx = await requestContextService.create({
  1049. apiType: 'admin',
  1050. });
  1051. const order = await orderService.create(ctx);
  1052. const warnSpy = vi.spyOn(Logger, 'warn');
  1053. await orderService.addItemToOrder(ctx, order.id, 1, 1);
  1054. expect(warnSpy).not.toHaveBeenCalled();
  1055. });
  1056. it('warns on unknown custom field in ProductTranslation entity', async () => {
  1057. const productService = server.app.get(ProductService);
  1058. const requestContextService = server.app.get(RequestContextService);
  1059. const ctx = await requestContextService.create({
  1060. apiType: 'admin',
  1061. });
  1062. const warnSpy = vi.spyOn(Logger, 'warn');
  1063. await productService.create(ctx, {
  1064. translations: [
  1065. {
  1066. languageCode: LanguageCode.en,
  1067. name: 'test',
  1068. slug: 'test',
  1069. description: '',
  1070. customFields: { customFieldWhichDoesNotExist: 'foo' },
  1071. },
  1072. ],
  1073. });
  1074. expect(warnSpy).toHaveBeenCalledWith(
  1075. 'Custom field customFieldWhichDoesNotExist not found for entity ProductTranslation',
  1076. );
  1077. });
  1078. it('does not warn when Translation has a valid custom field', async () => {
  1079. const productService = server.app.get(ProductService);
  1080. const requestContextService = server.app.get(RequestContextService);
  1081. const ctx = await requestContextService.create({
  1082. apiType: 'admin',
  1083. });
  1084. const warnSpy = vi.spyOn(Logger, 'warn');
  1085. await productService.create(ctx, {
  1086. translations: [
  1087. {
  1088. languageCode: LanguageCode.en,
  1089. name: 'test',
  1090. slug: 'test',
  1091. description: '',
  1092. customFields: { localeStringWithDefault: 'foo' },
  1093. },
  1094. ],
  1095. });
  1096. expect(warnSpy).not.toHaveBeenCalled();
  1097. });
  1098. });
  1099. });
  1100. const getServerConfigCustomFieldsDocument = graphql(`
  1101. query GetServerConfigCustomFields {
  1102. globalSettings {
  1103. serverConfig {
  1104. customFieldConfig {
  1105. Product {
  1106. ... on CustomField {
  1107. name
  1108. type
  1109. list
  1110. }
  1111. ... on RelationCustomFieldConfig {
  1112. scalarFields
  1113. }
  1114. }
  1115. }
  1116. }
  1117. }
  1118. }
  1119. `);
  1120. const getServerConfigEntityCustomFieldsDocument = graphql(`
  1121. query GetServerConfigEntityCustomFields {
  1122. globalSettings {
  1123. serverConfig {
  1124. entityCustomFields {
  1125. entityName
  1126. customFields {
  1127. ... on CustomField {
  1128. name
  1129. type
  1130. list
  1131. }
  1132. ... on RelationCustomFieldConfig {
  1133. scalarFields
  1134. }
  1135. }
  1136. }
  1137. }
  1138. }
  1139. }
  1140. `);
  1141. const getProductNullableDocument = graphql(`
  1142. query GetProductNullable($id: ID!) {
  1143. product(id: $id) {
  1144. id
  1145. name
  1146. customFields {
  1147. nullable
  1148. }
  1149. }
  1150. }
  1151. `);
  1152. const getProductWithDefaultsDocument = graphql(`
  1153. query GetProductWithDefaults($id: ID!) {
  1154. product(id: $id) {
  1155. id
  1156. name
  1157. customFields {
  1158. stringWithDefault
  1159. localeStringWithDefault
  1160. intWithDefault
  1161. floatWithDefault
  1162. booleanWithDefault
  1163. dateTimeWithDefault
  1164. stringListWithDefault
  1165. }
  1166. }
  1167. }
  1168. `);
  1169. const updateProductLongStringDocument = graphql(`
  1170. mutation UpdateProductLongString($id: ID!, $stringValue: String!) {
  1171. updateProduct(input: { id: $id, customFields: { longString: $stringValue } }) {
  1172. id
  1173. customFields {
  1174. longString
  1175. }
  1176. }
  1177. }
  1178. `);
  1179. const updateProductLongLocaleStringDocument = graphql(`
  1180. mutation UpdateProductLongLocaleString($id: ID!, $stringValue: String!) {
  1181. updateProduct(
  1182. input: {
  1183. id: $id
  1184. translations: [{ languageCode: en, customFields: { longLocaleString: $stringValue } }]
  1185. }
  1186. ) {
  1187. id
  1188. customFields {
  1189. longLocaleString
  1190. }
  1191. }
  1192. }
  1193. `);
  1194. const updateProductStringOptionsDocument = graphql(`
  1195. mutation UpdateProductStringOptions($id: ID!, $value: String!) {
  1196. updateProduct(input: { id: $id, customFields: { stringWithOptions: $value } }) {
  1197. id
  1198. customFields {
  1199. stringWithOptions
  1200. }
  1201. }
  1202. }
  1203. `);
  1204. const updateProductNullableStringOptionsDocument = graphql(`
  1205. mutation UpdateProductNullableStringOptions($id: ID!, $value: String) {
  1206. updateProduct(input: { id: $id, customFields: { nullableStringWithOptions: $value } }) {
  1207. id
  1208. customFields {
  1209. nullableStringWithOptions
  1210. }
  1211. }
  1212. }
  1213. `);
  1214. const updateProductIntListDocument = graphql(`
  1215. mutation UpdateProductIntList($id: ID!, $values: [Int!]!) {
  1216. updateProduct(input: { id: $id, customFields: { intListWithValidation: $values } }) {
  1217. id
  1218. customFields {
  1219. intListWithValidation
  1220. }
  1221. }
  1222. }
  1223. `);
  1224. const updateProductValidateFn3Document = graphql(`
  1225. mutation UpdateProductValidateFn3($id: ID!, $value: String!) {
  1226. updateProduct(input: { id: $id, customFields: { validateFn3: $value } }) {
  1227. id
  1228. customFields {
  1229. validateFn3
  1230. }
  1231. }
  1232. }
  1233. `);
  1234. const updateProductUniqueStringDocument = graphql(`
  1235. mutation UpdateProductUniqueString($id: ID!, $value: String!) {
  1236. updateProduct(input: { id: $id, customFields: { uniqueString: $value } }) {
  1237. id
  1238. customFields {
  1239. uniqueString
  1240. }
  1241. }
  1242. }
  1243. `);
  1244. const createProductUniqueStringDocument = graphql(`
  1245. mutation CreateProductUniqueString($uniqueString: String!) {
  1246. createProduct(
  1247. input: {
  1248. translations: [{ languageCode: en, name: "test 2", slug: "test-2", description: "" }]
  1249. customFields: { uniqueString: $uniqueString }
  1250. }
  1251. ) {
  1252. id
  1253. }
  1254. }
  1255. `);
  1256. const getProductVariantsWithCustomFieldsDocument = graphql(`
  1257. query GetProductVariantsWithCustomFields($productId: ID!) {
  1258. productVariants(productId: $productId) {
  1259. items {
  1260. product {
  1261. name
  1262. id
  1263. customFields {
  1264. localeStringWithDefault
  1265. stringWithDefault
  1266. }
  1267. }
  1268. }
  1269. }
  1270. }
  1271. `);
  1272. const getFacetCustomFieldsDocument = graphql(`
  1273. query GetFacetCustomFields($id: ID!) {
  1274. facet(id: $id) {
  1275. id
  1276. name
  1277. customFields {
  1278. translated
  1279. }
  1280. }
  1281. }
  1282. `);
  1283. const getCollectionCustomFieldsDocument = graphqlShop(`
  1284. query GetCollectionCustomFields($id: ID!) {
  1285. collection(id: $id) {
  1286. id
  1287. customFields
  1288. }
  1289. }
  1290. `);
  1291. const getProductsSortByNullableDocument = graphql(`
  1292. query GetProductsSortByNullable {
  1293. products(options: { sort: { nullable: ASC } }) {
  1294. totalItems
  1295. }
  1296. }
  1297. `);
  1298. const getProductsSortByLocaleStringDocument = graphql(`
  1299. query GetProductsSortByLocaleString {
  1300. products(options: { sort: { localeStringWithDefault: ASC } }) {
  1301. totalItems
  1302. }
  1303. }
  1304. `);
  1305. const getProductsFilterByStringDocument = graphql(`
  1306. query GetProductsFilterByString {
  1307. products(options: { filter: { stringWithDefault: { contains: "hello" } } }) {
  1308. totalItems
  1309. }
  1310. }
  1311. `);
  1312. const getProductsFilterByLocaleStringDocument = graphql(`
  1313. query GetProductsFilterByLocaleString {
  1314. products(options: { filter: { localeStringWithDefault: { contains: "hola" } } }) {
  1315. totalItems
  1316. }
  1317. }
  1318. `);
  1319. const getProductsFilterByIntListDocument = graphql(`
  1320. query GetProductsFilterByIntList($value: Float!) {
  1321. products(options: { filter: { intListWithValidation: { inList: $value } } }) {
  1322. totalItems
  1323. }
  1324. }
  1325. `);
  1326. const addItemToOrderWithCustomFieldsDocument = graphqlShop(`
  1327. mutation AddItemToOrderWithCustomFields($productVariantId: ID!, $quantity: Int!, $validateInt: Int!) {
  1328. addItemToOrder(productVariantId: $productVariantId, quantity: $quantity, customFields: { validateInt: $validateInt }) {
  1329. ... on Order {
  1330. id
  1331. lines {
  1332. customFields {
  1333. validateInt
  1334. }
  1335. }
  1336. }
  1337. }
  1338. }
  1339. `);
  1340. const adjustOrderLineWithCustomFieldsDocument = graphqlShop(`
  1341. mutation AdjustOrderLineWithCustomFields($orderLineId: ID!, $quantity: Int!, $validateInt: Int!) {
  1342. adjustOrderLine(orderLineId: $orderLineId, quantity: $quantity, customFields: { validateInt: $validateInt }) {
  1343. ... on Order {
  1344. id
  1345. lines {
  1346. customFields {
  1347. validateInt
  1348. }
  1349. }
  1350. }
  1351. }
  1352. }
  1353. `);
  1354. const updateProductVariantsWithPriceCustomFieldsDocument = graphql(`
  1355. mutation UpdateProductVariantsWithPriceCustomFields($input: [UpdateProductVariantInput!]!) {
  1356. updateProductVariants(input: $input) {
  1357. id
  1358. prices {
  1359. currencyCode
  1360. price
  1361. customFields {
  1362. costPrice
  1363. }
  1364. }
  1365. }
  1366. }
  1367. `);
  1368. const getShopProductPublicCustomFieldDocument = graphqlShop(`
  1369. query GetShopProductPublicCustomField($id: ID!) {
  1370. product(id: $id) {
  1371. id
  1372. customFields {
  1373. public
  1374. }
  1375. }
  1376. }
  1377. `);