mock-data.service.ts 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449
  1. import * as faker from 'faker/locale/en_GB';
  2. import * as fs from 'fs-extra';
  3. import gql from 'graphql-tag';
  4. import * as path from 'path';
  5. import {
  6. AddOptionGroupToProduct,
  7. Asset,
  8. Channel,
  9. CreateAddressInput,
  10. CreateChannel,
  11. CreateCountry,
  12. CreateCustomerInput,
  13. CreateFacet,
  14. CreateFacetValueWithFacetInput,
  15. CreateProduct,
  16. CreateProductOptionGroup,
  17. CreateTaxRate,
  18. CreateZone,
  19. GenerateProductVariants,
  20. GetChannels,
  21. LanguageCode,
  22. ProductTranslationInput,
  23. ProductVariant,
  24. UpdateChannel,
  25. UpdateProductVariants,
  26. Zone,
  27. } from 'shared/generated-types';
  28. import { CREATE_FACET } from '../../admin-ui/src/app/data/definitions/facet-definitions';
  29. import {
  30. ADD_OPTION_GROUP_TO_PRODUCT,
  31. CREATE_PRODUCT,
  32. CREATE_PRODUCT_OPTION_GROUP,
  33. GENERATE_PRODUCT_VARIANTS,
  34. UPDATE_PRODUCT_VARIANTS,
  35. } from '../../admin-ui/src/app/data/definitions/product-definitions';
  36. import {
  37. CREATE_CHANNEL,
  38. CREATE_COUNTRY,
  39. CREATE_TAX_RATE,
  40. CREATE_ZONE,
  41. GET_CHANNELS,
  42. UPDATE_CHANNEL,
  43. } from '../../admin-ui/src/app/data/definitions/settings-definitions';
  44. import { Customer } from '../src/entity/customer/customer.entity';
  45. import { SimpleGraphQLClient } from './simple-graphql-client';
  46. import TaxCategory = ProductVariant.TaxCategory;
  47. // tslint:disable:no-console
  48. /**
  49. * A service for creating mock data via the GraphQL API.
  50. */
  51. export class MockDataService {
  52. apiUrl: string;
  53. constructor(private client: SimpleGraphQLClient, private logging = true) {
  54. // make the generated results deterministic
  55. faker.seed(1);
  56. }
  57. async populateChannels(channelCodes: string[]): Promise<Channel.Fragment[]> {
  58. const channels: Channel.Fragment[] = [];
  59. for (const code of channelCodes) {
  60. const channel = await this.client.query<CreateChannel.Mutation, CreateChannel.Variables>(
  61. CREATE_CHANNEL,
  62. {
  63. input: {
  64. code,
  65. token: `${code}_token`,
  66. defaultLanguageCode: LanguageCode.en,
  67. },
  68. },
  69. );
  70. channels.push(channel.createChannel);
  71. this.log(`Created Channel: ${channel.createChannel.code}`);
  72. }
  73. return channels;
  74. }
  75. async populateCountries(): Promise<Zone.Fragment[]> {
  76. const countriesFile = await fs.readFile(
  77. path.join(__dirname, 'data-sources', 'countries.json'),
  78. 'utf8',
  79. );
  80. const countries: any[] = JSON.parse(countriesFile);
  81. const zones: { [zoneName: string]: string[] } = {};
  82. for (const country of countries) {
  83. const result = await this.client.query<CreateCountry.Mutation, CreateCountry.Variables>(
  84. CREATE_COUNTRY,
  85. {
  86. input: {
  87. code: country['alpha-2'],
  88. name: country.name,
  89. enabled: true,
  90. },
  91. },
  92. );
  93. if (!zones[country.region]) {
  94. zones[country.region] = [];
  95. }
  96. zones[country.region].push(result.createCountry.id);
  97. }
  98. const createdZones: Zone.Fragment[] = [];
  99. for (const [name, memberIds] of Object.entries(zones)) {
  100. const result = await this.client.query<CreateZone.Mutation, CreateZone.Variables>(CREATE_ZONE, {
  101. input: {
  102. name,
  103. memberIds,
  104. },
  105. });
  106. createdZones.push(result.createZone);
  107. }
  108. this.log(`Created ${countries.length} Countries in ${Object.keys(zones).length} Zones`);
  109. return createdZones;
  110. }
  111. async setChannelDefaultZones(zones: Zone.Fragment[]) {
  112. const defaultZone = zones.find(z => z.name === 'UK');
  113. if (!defaultZone) {
  114. this.log(`Default zone could not be found`);
  115. return;
  116. }
  117. const result = await this.client.query<GetChannels.Query>(GET_CHANNELS);
  118. for (const channel of result.channels) {
  119. await this.client.query<UpdateChannel.Mutation, UpdateChannel.Variables>(UPDATE_CHANNEL, {
  120. input: {
  121. id: channel.id,
  122. defaultTaxZoneId: defaultZone.id,
  123. defaultShippingZoneId: defaultZone.id,
  124. },
  125. });
  126. }
  127. this.log(`Set default zones for ${result.channels.length} Channels`);
  128. }
  129. async populateOptions(): Promise<string> {
  130. return this.client
  131. .query<CreateProductOptionGroup.Mutation, CreateProductOptionGroup.Variables>(
  132. CREATE_PRODUCT_OPTION_GROUP,
  133. {
  134. input: {
  135. code: 'size',
  136. translations: [
  137. { languageCode: LanguageCode.en, name: 'Size' },
  138. { languageCode: LanguageCode.de, name: 'Größe' },
  139. ],
  140. options: [
  141. {
  142. code: 'small',
  143. translations: [
  144. { languageCode: LanguageCode.en, name: 'Small' },
  145. { languageCode: LanguageCode.de, name: 'Klein' },
  146. ],
  147. },
  148. {
  149. code: 'large',
  150. translations: [
  151. { languageCode: LanguageCode.en, name: 'Large' },
  152. { languageCode: LanguageCode.de, name: 'Groß' },
  153. ],
  154. },
  155. ],
  156. },
  157. },
  158. )
  159. .then(data => {
  160. this.log('Created option group:', data.createProductOptionGroup.name);
  161. return data.createProductOptionGroup.id;
  162. });
  163. }
  164. async populateTaxCategories(zones: Zone.Fragment[]) {
  165. const taxCategories = [{ name: 'Standard Tax' }, { name: 'Reduced Tax' }, { name: 'Zero Tax' }];
  166. const createdTaxCategories: TaxCategory[] = [];
  167. for (const category of taxCategories) {
  168. const result = await this.client.query(
  169. gql`
  170. mutation($input: CreateTaxCategoryInput!) {
  171. createTaxCategory(input: $input) {
  172. id
  173. }
  174. }
  175. `,
  176. {
  177. input: {
  178. name: category.name,
  179. },
  180. },
  181. );
  182. createdTaxCategories.push(result.createTaxCategory);
  183. }
  184. this.log(`Created ${createdTaxCategories.length} tax categories`);
  185. // create tax rates
  186. for (const zone of zones) {
  187. await this.client.query<CreateTaxRate.Mutation, CreateTaxRate.Variables>(CREATE_TAX_RATE, {
  188. input: {
  189. name: `Standard Tax for ${zone.name}`,
  190. enabled: true,
  191. value: 20,
  192. categoryId: createdTaxCategories[0].id,
  193. zoneId: zone.id,
  194. },
  195. });
  196. }
  197. return createdTaxCategories;
  198. }
  199. async populateCustomers(count: number = 5): Promise<any> {
  200. for (let i = 0; i < count; i++) {
  201. const firstName = faker.name.firstName();
  202. const lastName = faker.name.lastName();
  203. const query1 = gql`
  204. mutation CreateCustomer($input: CreateCustomerInput!, $password: String) {
  205. createCustomer(input: $input, password: $password) {
  206. id
  207. emailAddress
  208. }
  209. }
  210. `;
  211. const variables1 = {
  212. input: {
  213. firstName,
  214. lastName,
  215. emailAddress: faker.internet.email(firstName, lastName),
  216. phoneNumber: faker.phone.phoneNumber(),
  217. } as CreateCustomerInput,
  218. password: 'test',
  219. };
  220. const customer: { id: string; emailAddress: string } | void = await this.client
  221. .query(query1, variables1)
  222. .then((data: any) => data.createCustomer, err => this.log(err));
  223. if (customer) {
  224. const query2 = gql`
  225. mutation($customerId: ID!, $input: CreateAddressInput!) {
  226. createCustomerAddress(customerId: $customerId, input: $input) {
  227. id
  228. streetLine1
  229. }
  230. }
  231. `;
  232. const variables2 = {
  233. input: {
  234. fullName: `${firstName} ${lastName}`,
  235. streetLine1: faker.address.streetAddress(),
  236. city: faker.address.city(),
  237. province: faker.address.county(),
  238. postalCode: faker.address.zipCode(),
  239. country: faker.address.country(),
  240. } as CreateAddressInput,
  241. customerId: customer.id,
  242. };
  243. await this.client.query(query2, variables2).then(
  244. data => {
  245. this.log(`Created Customer ${i + 1}:`, data);
  246. return data as Customer;
  247. },
  248. err => this.log(err),
  249. );
  250. }
  251. }
  252. }
  253. async populateAssets(): Promise<Asset[]> {
  254. const fileNames = await fs.readdir(path.join(__dirname, 'assets'));
  255. const filePaths = fileNames.map(fileName => path.join(__dirname, 'assets', fileName));
  256. return this.client.uploadAssets(filePaths).then(response => {
  257. this.log(`Created ${response.createAssets.length} Assets`);
  258. return response.createAssets;
  259. });
  260. }
  261. async populateProducts(
  262. count: number = 5,
  263. optionGroupId: string,
  264. assets: Asset[],
  265. taxCategories: TaxCategory[],
  266. ): Promise<any> {
  267. for (let i = 0; i < count; i++) {
  268. const query = CREATE_PRODUCT;
  269. const name = faker.commerce.productName();
  270. const slug = name.toLowerCase().replace(/\s+/g, '-');
  271. const description = faker.lorem.sentence();
  272. const languageCodes = [LanguageCode.en, LanguageCode.de];
  273. // get 2 (pseudo) random asset ids
  274. const randomAssets = this.shuffleArray(assets).slice(0, 2);
  275. const variables: CreateProduct.Variables = {
  276. input: {
  277. translations: languageCodes.map(code =>
  278. this.makeProductTranslation(code, name, slug, description),
  279. ),
  280. assetIds: randomAssets.map(a => a.id),
  281. featuredAssetId: randomAssets[0].id,
  282. },
  283. };
  284. const product = await this.client
  285. .query<CreateProduct.Mutation, CreateProduct.Variables>(query, variables)
  286. .then(
  287. data => {
  288. this.log(`Created Product ${i + 1}:`, data.createProduct.name);
  289. return data;
  290. },
  291. err => this.log(err),
  292. );
  293. if (product) {
  294. await this.client.query<AddOptionGroupToProduct.Mutation, AddOptionGroupToProduct.Variables>(
  295. ADD_OPTION_GROUP_TO_PRODUCT,
  296. {
  297. productId: product.createProduct.id,
  298. optionGroupId,
  299. },
  300. );
  301. const prodWithVariants = await this.makeProductVariant(
  302. product.createProduct.id,
  303. taxCategories[0],
  304. );
  305. const variants = prodWithVariants.generateVariantsForProduct.variants;
  306. for (const variant of variants) {
  307. const variantEN = variant.translations[0];
  308. const variantDE = { ...variantEN };
  309. variantDE.languageCode = LanguageCode.de;
  310. variantDE.name = variantDE.name.replace(LanguageCode.en, LanguageCode.de);
  311. delete variantDE.id;
  312. variant.translations.push(variantDE);
  313. }
  314. await this.client.query<UpdateProductVariants.Mutation, UpdateProductVariants.Variables>(
  315. UPDATE_PRODUCT_VARIANTS,
  316. {
  317. input: variants.map(({ id, translations, sku, price }) => ({
  318. id,
  319. translations,
  320. sku,
  321. price,
  322. })),
  323. },
  324. );
  325. }
  326. }
  327. }
  328. async populateFacets() {
  329. await this.client.query<CreateFacet.Mutation, CreateFacet.Variables>(CREATE_FACET, {
  330. input: {
  331. code: 'brand',
  332. translations: [
  333. {
  334. languageCode: LanguageCode.en,
  335. name: 'Brand',
  336. },
  337. {
  338. languageCode: LanguageCode.en,
  339. name: 'Marke',
  340. },
  341. ],
  342. values: this.makeFacetValues(10),
  343. },
  344. });
  345. this.log('Created "brand" Facet');
  346. }
  347. private makeFacetValues(count: number): CreateFacetValueWithFacetInput[] {
  348. return Array.from({ length: count }).map(() => {
  349. const brand = faker.company.companyName();
  350. return {
  351. code: brand.replace(/\s/g, '_'),
  352. translations: [
  353. {
  354. languageCode: LanguageCode.en,
  355. name: brand,
  356. },
  357. {
  358. languageCode: LanguageCode.de,
  359. name: brand,
  360. },
  361. ],
  362. };
  363. });
  364. }
  365. private makeProductTranslation(
  366. languageCode: LanguageCode,
  367. name: string,
  368. slug: string,
  369. description: string,
  370. ): ProductTranslationInput {
  371. return {
  372. languageCode,
  373. name: `${languageCode} ${name}`,
  374. slug: `${languageCode} ${slug}`,
  375. description: `${languageCode} ${description}`,
  376. };
  377. }
  378. private async makeProductVariant(
  379. productId: string,
  380. taxCategory: TaxCategory,
  381. ): Promise<GenerateProductVariants.Mutation> {
  382. const query = GENERATE_PRODUCT_VARIANTS;
  383. return this.client.query<GenerateProductVariants.Mutation, GenerateProductVariants.Variables>(query, {
  384. productId,
  385. defaultTaxCategoryId: taxCategory.id,
  386. defaultSku: faker.random.alphaNumeric(5),
  387. defaultPrice: faker.random.number({
  388. min: 100,
  389. max: 1000,
  390. }),
  391. });
  392. }
  393. private log(...args: any[]) {
  394. if (this.logging) {
  395. console.log(...args);
  396. }
  397. }
  398. /**
  399. * Deterministacally randomize array element order. Returns a new
  400. * shuffled array and leaves the input array intact.
  401. * Using Durstenfeld shuffle algorithm.
  402. *
  403. * Source: https://stackoverflow.com/a/12646864/772859
  404. */
  405. private shuffleArray<T>(array: T[]): T[] {
  406. const clone = array.slice(0);
  407. for (let i = clone.length - 1; i > 0; i--) {
  408. const j = Math.floor((faker.random.number(1000) / 1000) * (i + 1));
  409. const temp = clone[i];
  410. clone[i] = clone[j];
  411. clone[j] = temp;
  412. }
  413. return clone;
  414. }
  415. }