custom-field-struct.e2e-spec.ts 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448
  1. import { mergeConfig } from '@vendure/core';
  2. import { createTestEnvironment } from '@vendure/testing';
  3. import gql from 'graphql-tag';
  4. import path from 'path';
  5. import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest';
  6. import { initialData } from '../../../e2e-common/e2e-initial-data';
  7. import { TEST_SETUP_TIMEOUT_MS, testConfig } 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. const validateInjectorSpy = vi.fn();
  12. const customConfig = mergeConfig(testConfig(), {
  13. dbConnectionOptions: {
  14. timezone: 'Z',
  15. },
  16. customFields: {
  17. Product: [
  18. {
  19. name: 'attributes',
  20. type: 'struct',
  21. fields: [
  22. { name: 'color', type: 'string' },
  23. { name: 'size', type: 'string' },
  24. { name: 'material', type: 'string' },
  25. { name: 'weight', type: 'int' },
  26. { name: 'isDownloadable', type: 'boolean' },
  27. { name: 'releaseDate', type: 'datetime' },
  28. ],
  29. },
  30. ],
  31. Customer: [
  32. {
  33. name: 'coupons',
  34. type: 'struct',
  35. list: true,
  36. fields: [
  37. { name: 'code', type: 'string' },
  38. { name: 'discount', type: 'int' },
  39. { name: 'used', type: 'boolean' },
  40. ],
  41. },
  42. {
  43. name: 'company',
  44. type: 'struct',
  45. fields: [{ name: 'phoneNumbers', type: 'string', list: true }],
  46. },
  47. {
  48. name: 'withValidation',
  49. type: 'struct',
  50. fields: [
  51. { name: 'stringWithPattern', type: 'string', pattern: '^[0-9][a-z]+$' },
  52. { name: 'numberWithRange', type: 'int', min: 1, max: 10 },
  53. {
  54. name: 'stringWithValidationFn',
  55. type: 'string',
  56. validate: value => {
  57. if (value !== 'valid') {
  58. return `The value ['${value as string}'] is not valid`;
  59. }
  60. },
  61. },
  62. ],
  63. },
  64. ],
  65. },
  66. });
  67. describe('Custom field struct type', () => {
  68. const { server, adminClient, shopClient } = createTestEnvironment(customConfig);
  69. beforeAll(async () => {
  70. await server.init({
  71. initialData,
  72. productsCsvPath: path.join(__dirname, 'fixtures/e2e-products-minimal.csv'),
  73. customerCount: 1,
  74. });
  75. await adminClient.asSuperAdmin();
  76. }, TEST_SETUP_TIMEOUT_MS);
  77. afterAll(async () => {
  78. await server.destroy();
  79. });
  80. it('globalSettings.serverConfig.customFieldConfig resolves struct fields', async () => {
  81. const { globalSettings } = await adminClient.query(gql`
  82. query {
  83. globalSettings {
  84. serverConfig {
  85. customFieldConfig {
  86. Product {
  87. ... on CustomField {
  88. name
  89. type
  90. list
  91. }
  92. ... on StructCustomFieldConfig {
  93. fields {
  94. ... on StructField {
  95. name
  96. type
  97. list
  98. }
  99. }
  100. }
  101. }
  102. }
  103. }
  104. }
  105. }
  106. `);
  107. expect(globalSettings.serverConfig.customFieldConfig.Product).toEqual([
  108. {
  109. name: 'attributes',
  110. type: 'struct',
  111. list: false,
  112. fields: [
  113. { name: 'color', type: 'string', list: false },
  114. { name: 'size', type: 'string', list: false },
  115. { name: 'material', type: 'string', list: false },
  116. { name: 'weight', type: 'int', list: false },
  117. { name: 'isDownloadable', type: 'boolean', list: false },
  118. { name: 'releaseDate', type: 'datetime', list: false },
  119. ],
  120. },
  121. ]);
  122. });
  123. it('globalSettings.serverConfig.entityCustomFields resolves struct fields', async () => {
  124. const { globalSettings } = await adminClient.query(gql`
  125. query {
  126. globalSettings {
  127. serverConfig {
  128. entityCustomFields {
  129. entityName
  130. customFields {
  131. ... on CustomField {
  132. name
  133. type
  134. list
  135. }
  136. ... on StructCustomFieldConfig {
  137. fields {
  138. ... on StructField {
  139. name
  140. type
  141. list
  142. }
  143. }
  144. }
  145. }
  146. }
  147. }
  148. }
  149. }
  150. `);
  151. const productEntry = globalSettings.serverConfig.entityCustomFields.find(
  152. (e: any) => e.entityName === 'Product',
  153. );
  154. expect(productEntry).toEqual({
  155. entityName: 'Product',
  156. customFields: [
  157. {
  158. name: 'attributes',
  159. type: 'struct',
  160. list: false,
  161. fields: [
  162. { name: 'color', type: 'string', list: false },
  163. { name: 'size', type: 'string', list: false },
  164. { name: 'material', type: 'string', list: false },
  165. { name: 'weight', type: 'int', list: false },
  166. { name: 'isDownloadable', type: 'boolean', list: false },
  167. { name: 'releaseDate', type: 'datetime', list: false },
  168. ],
  169. },
  170. ],
  171. });
  172. });
  173. it('struct fields initially null', async () => {
  174. const result = await adminClient.query(gql`
  175. query {
  176. product(id: "T_1") {
  177. id
  178. customFields {
  179. attributes {
  180. color
  181. size
  182. material
  183. weight
  184. isDownloadable
  185. releaseDate
  186. }
  187. }
  188. }
  189. }
  190. `);
  191. expect(result.product.customFields.attributes).toEqual({
  192. color: null,
  193. size: null,
  194. material: null,
  195. weight: null,
  196. isDownloadable: null,
  197. releaseDate: null,
  198. });
  199. });
  200. it('update all fields in struct', async () => {
  201. const result = await adminClient.query(gql`
  202. mutation {
  203. updateProduct(
  204. input: {
  205. id: "T_1"
  206. customFields: {
  207. attributes: {
  208. color: "red"
  209. size: "L"
  210. material: "cotton"
  211. weight: 123
  212. isDownloadable: true
  213. releaseDate: "2021-01-01T12:00:00.000Z"
  214. }
  215. }
  216. }
  217. ) {
  218. id
  219. customFields {
  220. attributes {
  221. color
  222. size
  223. material
  224. weight
  225. isDownloadable
  226. releaseDate
  227. }
  228. }
  229. }
  230. }
  231. `);
  232. expect(result.updateProduct.customFields.attributes).toEqual({
  233. color: 'red',
  234. size: 'L',
  235. material: 'cotton',
  236. weight: 123,
  237. isDownloadable: true,
  238. releaseDate: '2021-01-01T12:00:00.000Z',
  239. });
  240. });
  241. it('partial update of struct fields nulls missing fields', async () => {
  242. const result = await adminClient.query(gql`
  243. mutation {
  244. updateProduct(
  245. input: {
  246. id: "T_1"
  247. customFields: { attributes: { color: "red", size: "L", material: "cotton" } }
  248. }
  249. ) {
  250. id
  251. customFields {
  252. attributes {
  253. color
  254. size
  255. material
  256. weight
  257. isDownloadable
  258. releaseDate
  259. }
  260. }
  261. }
  262. }
  263. `);
  264. expect(result.updateProduct.customFields.attributes).toEqual({
  265. color: 'red',
  266. size: 'L',
  267. material: 'cotton',
  268. weight: null,
  269. isDownloadable: null,
  270. releaseDate: null,
  271. });
  272. });
  273. describe('struct list', () => {
  274. it('is initially an empty array', async () => {
  275. const result = await adminClient.query(gql`
  276. query {
  277. customer(id: "T_1") {
  278. customFields {
  279. coupons {
  280. code
  281. discount
  282. used
  283. }
  284. }
  285. }
  286. }
  287. `);
  288. expect(result.customer.customFields.coupons).toEqual([]);
  289. });
  290. it('sets list values', async () => {
  291. const result = await adminClient.query(gql`
  292. mutation {
  293. updateCustomer(
  294. input: {
  295. id: "T_1"
  296. customFields: {
  297. coupons: [
  298. { code: "ABC", discount: 10, used: false }
  299. { code: "DEF", discount: 20, used: true }
  300. ]
  301. }
  302. }
  303. ) {
  304. ... on Customer {
  305. id
  306. customFields {
  307. coupons {
  308. code
  309. discount
  310. used
  311. }
  312. }
  313. }
  314. }
  315. }
  316. `);
  317. expect(result.updateCustomer.customFields).toEqual({
  318. coupons: [
  319. { code: 'ABC', discount: 10, used: false },
  320. { code: 'DEF', discount: 20, used: true },
  321. ],
  322. });
  323. });
  324. });
  325. describe('struct field list', () => {
  326. it('is initially an empty array', async () => {
  327. const result = await adminClient.query(gql`
  328. query {
  329. customer(id: "T_1") {
  330. id
  331. customFields {
  332. company {
  333. phoneNumbers
  334. }
  335. }
  336. }
  337. }
  338. `);
  339. expect(result.customer.customFields.company).toEqual({
  340. phoneNumbers: [],
  341. });
  342. });
  343. it('set list field values', async () => {
  344. const result = await adminClient.query(gql`
  345. mutation {
  346. updateCustomer(
  347. input: { id: "T_1", customFields: { company: { phoneNumbers: ["123", "456"] } } }
  348. ) {
  349. ... on Customer {
  350. id
  351. customFields {
  352. company {
  353. phoneNumbers
  354. }
  355. }
  356. }
  357. }
  358. }
  359. `);
  360. expect(result.updateCustomer.customFields.company).toEqual({
  361. phoneNumbers: ['123', '456'],
  362. });
  363. });
  364. });
  365. describe('struct field validation', () => {
  366. it(
  367. 'string pattern',
  368. assertThrowsWithMessage(async () => {
  369. await adminClient.query(gql`
  370. mutation {
  371. updateCustomer(
  372. input: {
  373. id: "T_1"
  374. customFields: { withValidation: { stringWithPattern: "abc" } }
  375. }
  376. ) {
  377. ... on Customer {
  378. id
  379. }
  380. }
  381. }
  382. `);
  383. }, `The custom field "stringWithPattern" value ["abc"] does not match the pattern [^[0-9][a-z]+$]`),
  384. );
  385. it(
  386. 'number range',
  387. assertThrowsWithMessage(async () => {
  388. await adminClient.query(gql`
  389. mutation {
  390. updateCustomer(
  391. input: { id: "T_1", customFields: { withValidation: { numberWithRange: 15 } } }
  392. ) {
  393. ... on Customer {
  394. id
  395. }
  396. }
  397. }
  398. `);
  399. }, `The custom field "numberWithRange" value [15] is greater than the maximum [10]`),
  400. );
  401. it(
  402. 'validate function',
  403. assertThrowsWithMessage(async () => {
  404. await adminClient.query(gql`
  405. mutation {
  406. updateCustomer(
  407. input: {
  408. id: "T_1"
  409. customFields: { withValidation: { stringWithValidationFn: "bad" } }
  410. }
  411. ) {
  412. ... on Customer {
  413. id
  414. }
  415. }
  416. }
  417. `);
  418. }, `The value ['bad'] is not valid`),
  419. );
  420. });
  421. });