mock-data.service.ts 15 KB

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