make-mp-data 1.0.15 โ†’ 1.0.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,8 @@
1
1
  {
2
2
  "cSpell.words": [
3
3
  "unparse"
4
- ]
4
+ ],
5
+ "jest.runMode": "on-demand",
6
+ "jest.jestCommandLine": "npm run test --",
7
+ "js/ts.implicitProjectConfig.checkJs": false,
5
8
  }
package/cli.js CHANGED
@@ -25,38 +25,45 @@ DOCS: https://github.com/ak--47/make-mp-data`)
25
25
  .command('$0', 'model mixpanel data', () => { })
26
26
  .option("token", {
27
27
  demandOption: false,
28
+ alias: 't',
28
29
  describe: 'project token; if supplied data will be sent to mixpanel',
29
30
  type: 'string'
30
31
  })
31
32
  .option("seed", {
32
- demandOption: false,
33
+ demandOption: false,
34
+ alias: 's',
33
35
  describe: 'randomness seed; used to create distinct_ids',
34
36
  type: 'string'
35
37
  })
36
38
  .option("format", {
37
39
  demandOption: false,
38
40
  default: 'csv',
41
+ alias: 'f',
39
42
  describe: 'csv or json',
40
43
  type: 'string'
41
44
  })
42
45
  .option("numDays", {
43
46
  demandOption: false,
47
+ alias: 'd',
44
48
  describe: 'number of days in past to model',
45
49
  type: 'number',
46
50
  })
47
51
  .option("numUsers", {
48
52
  demandOption: false,
53
+ alias: 'u',
49
54
  describe: 'number of users to model',
50
55
  type: 'number',
51
56
  })
52
57
  .option("numEvents", {
53
58
  demandOption: false,
59
+ alias: 'e',
54
60
  describe: 'number of events to model',
55
61
  type: 'number',
56
62
  })
57
63
  .option("region", {
58
64
  demandOption: false,
59
65
  default: 'US',
66
+ alias: 'r',
60
67
  describe: 'either US or EU',
61
68
  type: 'string'
62
69
  })
package/default.js CHANGED
@@ -1,6 +1,6 @@
1
1
  const Chance = require('chance');
2
2
  const chance = new Chance();
3
- const { weightedRange, makeProducts, date } = require('./utils.js');
3
+ const { weightedRange, makeProducts, date, generateEmoji } = require('./utils.js');
4
4
 
5
5
  const config = {
6
6
  token: "",
@@ -36,7 +36,8 @@ const config = {
36
36
  "event": "page view",
37
37
  "weight": 10,
38
38
  "properties": {
39
- path: ["/", "/", "/help", "/account", "/watch", "/listen", "/product", "/people", "/peace"],
39
+ page: ["/", "/", "/help", "/account", "/watch", "/listen", "/product", "/people", "/peace"],
40
+ utm_source: ["$organic", "$organic", "$organic", "$organic", "google", "google", "google", "facebook", "facebook", "twitter", "linkedin"],
40
41
  }
41
42
  },
42
43
  {
@@ -66,7 +67,8 @@ const config = {
66
67
  }
67
68
  ],
68
69
  superProps: {
69
- platform: ["web", "mobile", "kiosk"],
70
+ platform: ["web", "mobile", "web", "mobile", "web", "kiosk"],
71
+ emotions: generateEmoji(),
70
72
 
71
73
  },
72
74
  /*
@@ -76,17 +78,16 @@ const config = {
76
78
  userProps: {
77
79
  title: chance.profession.bind(chance),
78
80
  luckyNumber: weightedRange(42, 420),
79
- servicesUsed: [["foo"], ["foo", "bar"], ["foo", "bar", "baz"], ["foo", "bar", "baz", "qux"], ["baz", "qux"], ["qux"]],
81
+ vibe: generateEmoji(),
80
82
  spiritAnimal: chance.animal.bind(chance)
81
83
  },
82
84
 
83
85
  scdProps: {
84
- plan: ["free", "free", "free", "basic", "basic", "premium", "enterprise"],
86
+ plan: ["free", "free", "free", "free", "basic", "basic", "basic", "premium", "premium", "enterprise"],
85
87
  MRR: weightedRange(0, 10000, 1000, .15),
86
88
  NPS: weightedRange(0, 10, 150, 2),
87
89
  marketingOptIn: [true, true, false],
88
90
  dateOfRenewal: date(100, false),
89
-
90
91
  },
91
92
 
92
93
  /*
package/e2e.test.js ADDED
@@ -0,0 +1,50 @@
1
+ /* cSpell:disable */
2
+ // @ts-nocheck
3
+ /* eslint-disable no-undef */
4
+ /* eslint-disable no-debugger */
5
+ /* eslint-disable no-unused-vars */
6
+ const { generate } = require('./index.js');
7
+ require('dotenv').config();
8
+ const { execSync } = require("child_process");
9
+ const u = require('ak-tools');
10
+
11
+ const timeout = 60000;
12
+
13
+
14
+ describe('e2e', () => {
15
+
16
+ test('works as module', async () => {
17
+ console.log('MODULE TEST');
18
+ const results = await generate({ writeToDisk: false, numEvents: 1000, numUsers: 100, seed: "deal with it" });
19
+ const { eventData, groupProfilesData, lookupTableData, scdTableData, userProfilesData } = results;
20
+ expect(eventData.length).toBeGreaterThan(900);
21
+ expect(groupProfilesData.length).toBe(0);
22
+ expect(lookupTableData.length).toBe(0);
23
+ expect(scdTableData.length).toBeGreaterThan(200);
24
+ expect(userProfilesData.length).toBe(100);
25
+
26
+ }, timeout);
27
+
28
+ test('works as CLI', async () => {
29
+ console.log('CLI TEST');
30
+ const run = execSync(`node ./index.js --numEvents 1000 --numUsers 100 --seed "deal with it"`);
31
+ expect(run.toString().trim().includes('have a wonderful day :)')).toBe(true);
32
+ const csvs = (await u.ls('./data')).filter(a => a.includes('.csv'));
33
+ expect(csvs.length).toBe(5);
34
+ }, timeout);
35
+
36
+
37
+
38
+ });
39
+
40
+
41
+
42
+ afterEach(() => {
43
+
44
+ });
45
+
46
+ afterAll(() => {
47
+ console.log('clearing...');
48
+ execSync(`npm run prune`);
49
+ console.log('...files cleared ๐Ÿ‘');
50
+ });
package/index.js CHANGED
@@ -1,11 +1,13 @@
1
1
  #! /usr/bin/env node
2
2
 
3
+
3
4
  /*
4
5
  make fake mixpanel data easily!
5
6
  by AK
6
7
  ak@mixpanel.com
7
8
  */
8
9
 
10
+ const RUNTIME = process.env.RUNTIME || "unspecified";
9
11
  const mp = require("mixpanel-import");
10
12
  const path = require("path");
11
13
  const Chance = require("chance");
@@ -27,14 +29,18 @@ const {
27
29
  openFinder,
28
30
  applySkew,
29
31
  boxMullerRandom,
32
+ getUniqueKeys
30
33
  } = require("./utils.js");
31
34
  const dayjs = require("dayjs");
32
35
  const utc = require("dayjs/plugin/utc");
33
36
  dayjs.extend(utc);
34
37
  const cliParams = require("./cli.js");
38
+ // @ts-ignore
35
39
  Array.prototype.pickOne = pick;
36
40
  const NOW = dayjs().unix();
37
41
 
42
+ /** @typedef {import('./types.d.ts').Config} Config */
43
+
38
44
  const PEAK_DAYS = [
39
45
  dayjs().subtract(2, "day").unix(),
40
46
  dayjs().subtract(3, "day").unix(),
@@ -48,7 +54,10 @@ const PEAK_DAYS = [
48
54
  dayjs().subtract(29, "day").unix(),
49
55
  ];
50
56
 
51
- //our main program
57
+ /**
58
+ * generates fake mixpanel data
59
+ * @param {Config} config
60
+ */
52
61
  async function main(config) {
53
62
  let {
54
63
  seed = "every time a rug is micturated upon in this fair city...",
@@ -70,29 +79,30 @@ async function main(config) {
70
79
  region = "US",
71
80
  writeToDisk = false,
72
81
  } = config;
73
-
82
+
74
83
  //ensure we have a token or are writing to disk
75
84
  if (require.main === module) {
76
85
  if (!token) {
77
86
  if (!writeToDisk) {
78
87
  writeToDisk = true;
88
+ config.writeToDisk = true;
79
89
  }
80
90
  }
81
91
  }
82
-
92
+
83
93
  const uuidChance = new Chance(seed);
84
94
 
85
- //the function which generates $distinct_id + $created
95
+ //the function which generates $distinct_id + $created, skewing towards the present
86
96
  function uuid() {
87
97
  const distinct_id = uuidChance.guid();
88
98
  let z = boxMullerRandom();
89
- const skew = chance.normal({ mean: 10, dev: 3 });
90
- z = applySkew(z, skew);
99
+ const skew = chance.normal({ mean: 10, dev: 3 });
100
+ z = applySkew(z, skew);
91
101
 
92
102
  // Scale and shift the normally distributed value to fit the range of days
93
- const maxZ = integer(2, 4);
94
- const scaledZ = (z / maxZ + 1) / 2; // Scale to a 0-1 range
95
- const daysAgoBorn = Math.round(scaledZ * (numDays - 1)) + 1; // Scale to 1-numDays range
103
+ const maxZ = integer(2, 4);
104
+ const scaledZ = (z / maxZ + 1) / 2;
105
+ const daysAgoBorn = Math.round(scaledZ * (numDays - 1)) + 1;
96
106
 
97
107
  return {
98
108
  distinct_id,
@@ -223,7 +233,14 @@ async function main(config) {
223
233
  for (const writeData of datasetsToWrite) {
224
234
  if (format === "csv") {
225
235
  console.log(`writing ${path}`);
226
- const csv = Papa.unparse(writeData, {});
236
+ const columns = getUniqueKeys(writeData);
237
+ //papa parse needs nested JSON stringified
238
+ writeData.forEach((e) => {
239
+ for (const key in e) {
240
+ if (typeof e[key] === "object") e[key] = JSON.stringify(e[key]);
241
+ }
242
+ })
243
+ const csv = Papa.unparse(writeData, { columns });
227
244
  await touch(path, csv);
228
245
  console.log(`\tdone\n`);
229
246
  } else {
@@ -238,13 +255,13 @@ async function main(config) {
238
255
  const creds = { token };
239
256
  /** @type {import('mixpanel-import').Options} */
240
257
  const importOpts = {
258
+ region,
241
259
  fixData: true,
242
260
  verbose: false,
243
261
  forceStream: true,
244
262
  strict: false,
245
263
  dryRun: false,
246
264
  abridged: false,
247
- region,
248
265
  };
249
266
  //send to mixpanel
250
267
  if (token) {
@@ -252,6 +269,9 @@ async function main(config) {
252
269
  console.log(`importing events to mixpanel...`);
253
270
  const imported = await mp(creds, eventData, {
254
271
  recordType: "event",
272
+ fixData: true,
273
+ fixJson: true,
274
+ strict: false,
255
275
  ...importOpts,
256
276
  });
257
277
  console.log(`\tsent ${comma(imported.success)} events\n`);
@@ -331,10 +351,10 @@ function makeEvent(distinct_id, earliestTime, events, superProps, groupKeys, isF
331
351
  const event = {
332
352
  event: chosenEvent.event,
333
353
  distinct_id,
334
- $source: "AK's fake data generator",
354
+ $source: "AKsTimeSoup",
335
355
  };
336
356
 
337
- if (isFirstEvent) event.time = earliestTime;
357
+ if (isFirstEvent) event.time = dayjs.unix(earliestTime).toISOString();
338
358
  if (!isFirstEvent) event.time = AKsTimeSoup(earliestTime, NOW, PEAK_DAYS);
339
359
 
340
360
  const props = { ...chosenEvent.properties, ...superProps };
@@ -392,7 +412,7 @@ function buildFileNames(config) {
392
412
  }
393
413
 
394
414
  /**
395
- * timestamp generator with a twist
415
+ * essentially, a timestamp generator with a twist
396
416
  * @param {number} earliestTime - The earliest timestamp in Unix format.
397
417
  * @param {number} latestTime - The latest timestamp in Unix format.
398
418
  * @param {Array} peakDays - Array of Unix timestamps representing the start of peak days.
@@ -432,8 +452,8 @@ function AKsTimeSoup(earliestTime, latestTime = NOW, peakDays = PEAK_DAYS) {
432
452
 
433
453
  // usually, ensure the event time is within business hours
434
454
  if (chance.bool({ likelihood: 42 })) eventTime = Math.min(Math.max(eventTime, businessStart), businessEnd);
435
-
436
- return eventTime;
455
+
456
+ return dayjs.unix(eventTime).toISOString();
437
457
  }
438
458
 
439
459
 
package/package.json CHANGED
@@ -1,13 +1,15 @@
1
1
  {
2
2
  "name": "make-mp-data",
3
- "version": "1.0.15",
3
+ "version": "1.0.17",
4
4
  "description": "builds all mixpanel primitives for a given project",
5
5
  "main": "index.js",
6
+ "types": "types.d.ts",
6
7
  "scripts": {
7
8
  "start": "node index.js",
8
9
  "prune": "rm ./data/*",
9
10
  "go": "sh ./go.sh",
10
- "post": "npm publish"
11
+ "post": "npm publish",
12
+ "test": "jest"
11
13
  },
12
14
  "repository": {
13
15
  "type": "git",
package/types.d.ts ADDED
@@ -0,0 +1,53 @@
1
+ // types.d.ts
2
+ import { Chance } from "chance";
3
+
4
+ export interface Config {
5
+ token?: string;
6
+ seed?: string;
7
+ numDays?: number;
8
+ numEvents?: number;
9
+ numUsers?: number;
10
+ format?: "csv" | "json";
11
+ region?: string;
12
+ events?: EventConfig[];
13
+ superProps?: Record<string, string[]>; // Flexible for any string keys
14
+ userProps?: Record<string, any>; // Could be more specific based on actual usage
15
+ scdProps?: {
16
+ plan?: string[];
17
+ MRR?: number;
18
+ NPS?: number;
19
+ marketingOptIn?: boolean[];
20
+ dateOfRenewal?: Date;
21
+ };
22
+ groupKeys?: [string, number][];
23
+ groupProps?: Record<string, GroupProperty>; // Adjust according to usage
24
+ lookupTables?: LookupTable[];
25
+ writeToDisk?: boolean;
26
+ }
27
+
28
+ interface EventConfig {
29
+ event?: string;
30
+ weight?: number;
31
+ properties?: {
32
+ [key: string]: any; // Consider refining based on actual properties used
33
+ };
34
+ isFirstEvent?: boolean;
35
+ }
36
+
37
+ interface GroupProperty {
38
+ [key?: string]: any;
39
+ }
40
+
41
+ interface LookupTable {
42
+ key?: string;
43
+ entries?: number;
44
+ attributes?: {
45
+ category?: string[];
46
+ demand?: string[];
47
+ supply?: string[];
48
+ manufacturer?: () => string;
49
+ price?: number;
50
+ rating?: number;
51
+ reviews?: number;
52
+ };
53
+ }
package/utils.js CHANGED
@@ -20,12 +20,28 @@ function pick() {
20
20
  function date(inTheLast = 30, isPast = true, format = 'YYYY-MM-DD') {
21
21
  const now = dayjs.utc();
22
22
  return function () {
23
- const when = chance.integer({ min: 0, max: Math.abs(inTheLast) });
24
- let then;
25
- if (isPast) then = now.subtract(when, 'day');
26
- if (!isPast) then = now.add(when, 'day');
27
- if (format) return then.format(format);
28
- if (!format) return then.toISOString();
23
+ try {
24
+ const when = chance.integer({ min: 0, max: Math.abs(inTheLast) });
25
+ let then;
26
+ if (isPast) {
27
+ then = now.subtract(when, 'day')
28
+ .subtract(integer(0, 23), 'hour')
29
+ .subtract(integer(0, 59), 'minute')
30
+ .subtract(integer(0, 59), 'second');
31
+ }
32
+ if (!isPast) {
33
+ then = now.add(when, 'day')
34
+ .add(integer(0, 23), 'hour')
35
+ .add(integer(0, 59), 'minute')
36
+ .add(integer(0, 59), 'second');
37
+ }
38
+ if (format) return then?.format(format);
39
+ if (!format) return then?.toISOString();
40
+ }
41
+ catch (e) {
42
+ if (format) return now?.format(format);
43
+ if (!format) return now?.toISOString();
44
+ }
29
45
  };
30
46
  }
31
47
 
@@ -74,6 +90,10 @@ function exhaust(arr) {
74
90
 
75
91
 
76
92
  function integer(min, max) {
93
+ if (min === max) {
94
+ return min;
95
+ }
96
+
77
97
  if (min > max) {
78
98
  return chance.integer({
79
99
  min: max,
@@ -81,10 +101,14 @@ function integer(min, max) {
81
101
  });
82
102
  }
83
103
 
84
- return chance.integer({
85
- min: min,
86
- max: max
87
- });
104
+ if (min < max) {
105
+ return chance.integer({
106
+ min: min,
107
+ max: max
108
+ });
109
+ }
110
+
111
+ return 0;
88
112
  }
89
113
 
90
114
  function makeProducts() {
@@ -168,7 +192,6 @@ function progress(thing, p) {
168
192
  process.stdout.write(`${thing} processed ... ${comma(p)}`);
169
193
  }
170
194
 
171
-
172
195
  function person(bornDaysAgo = 30) {
173
196
  //names and photos
174
197
  const gender = chance.pickone(['male', 'female']);
@@ -213,7 +236,27 @@ function openFinder(path, callback) {
213
236
  });
214
237
  }
215
238
 
239
+ function getUniqueKeys(data) {
240
+ const keysSet = new Set();
241
+ data.forEach(item => {
242
+ Object.keys(item).forEach(key => keysSet.add(key));
243
+ });
244
+ return Array.from(keysSet);
245
+ }
216
246
 
247
+ //makes a random-sized array of emojis
248
+ function generateEmoji(max = 10, array = false) {
249
+ return function () {
250
+ const emojis = ['๐Ÿ˜€', '๐Ÿ˜‚', '๐Ÿ˜', '๐Ÿ˜Ž', '๐Ÿ˜œ', '๐Ÿ˜‡', '๐Ÿ˜ก', '๐Ÿ˜ฑ', '๐Ÿ˜ญ', '๐Ÿ˜ด', '๐Ÿคข', '๐Ÿค ', '๐Ÿคก', '๐Ÿ‘ฝ', '๐Ÿ‘ป', '๐Ÿ’ฉ', '๐Ÿ‘บ', '๐Ÿ‘น', '๐Ÿ‘พ', '๐Ÿค–', '๐Ÿค‘', '๐Ÿค—', '๐Ÿค“', '๐Ÿค”', '๐Ÿค', '๐Ÿ˜€', '๐Ÿ˜‚', '๐Ÿ˜', '๐Ÿ˜Ž', '๐Ÿ˜œ', '๐Ÿ˜‡', '๐Ÿ˜ก', '๐Ÿ˜ฑ', '๐Ÿ˜ญ', '๐Ÿ˜ด', '๐Ÿคข', '๐Ÿค ', '๐Ÿคก', '๐Ÿ‘ฝ', '๐Ÿ‘ป', '๐Ÿ’ฉ', '๐Ÿ‘บ', '๐Ÿ‘น', '๐Ÿ‘พ', '๐Ÿค–', '๐Ÿค‘', '๐Ÿค—', '๐Ÿค“', '๐Ÿค”', '๐Ÿค', '๐Ÿ˜ˆ', '๐Ÿ‘ฟ', '๐Ÿ‘ฆ', '๐Ÿ‘ง', '๐Ÿ‘จ', '๐Ÿ‘ฉ', '๐Ÿ‘ด', '๐Ÿ‘ต', '๐Ÿ‘ถ', '๐Ÿง’', '๐Ÿ‘ฎ', '๐Ÿ‘ท', '๐Ÿ’‚', '๐Ÿ•ต', '๐Ÿ‘ฉโ€โš•๏ธ', '๐Ÿ‘จโ€โš•๏ธ', '๐Ÿ‘ฉโ€๐ŸŒพ', '๐Ÿ‘จโ€๐ŸŒพ', '๐Ÿ‘ฉโ€๐Ÿณ', '๐Ÿ‘จโ€๐Ÿณ', '๐Ÿ‘ฉโ€๐ŸŽ“', '๐Ÿ‘จโ€๐ŸŽ“', '๐Ÿ‘ฉโ€๐ŸŽค', '๐Ÿ‘จโ€๐ŸŽค', '๐Ÿ‘ฉโ€๐Ÿซ', '๐Ÿ‘จโ€๐Ÿซ', '๐Ÿ‘ฉโ€๐Ÿญ', '๐Ÿ‘จโ€๐Ÿญ', '๐Ÿ‘ฉโ€๐Ÿ’ป', '๐Ÿ‘จโ€๐Ÿ’ป', '๐Ÿ‘ฉโ€๐Ÿ’ผ', '๐Ÿ‘จโ€๐Ÿ’ผ', '๐Ÿ‘ฉโ€๐Ÿ”ง', '๐Ÿ‘จโ€๐Ÿ”ง', '๐Ÿ‘ฉโ€๐Ÿ”ฌ', '๐Ÿ‘จโ€๐Ÿ”ฌ', '๐Ÿ‘ฉโ€๐ŸŽจ', '๐Ÿ‘จโ€๐ŸŽจ', '๐Ÿ‘ฉโ€๐Ÿš’', '๐Ÿ‘จโ€๐Ÿš’', '๐Ÿ‘ฉโ€โœˆ๏ธ', '๐Ÿ‘จโ€โœˆ๏ธ', '๐Ÿ‘ฉโ€๐Ÿš€', '๐Ÿ‘จโ€๐Ÿš€', '๐Ÿ‘ฉโ€โš–๏ธ', '๐Ÿ‘จโ€โš–๏ธ', '๐Ÿคถ', '๐ŸŽ…', '๐Ÿ‘ธ', '๐Ÿคด', '๐Ÿ‘ฐ', '๐Ÿคต', '๐Ÿ‘ผ', '๐Ÿคฐ', '๐Ÿ™‡', '๐Ÿ’', '๐Ÿ™…', '๐Ÿ™†', '๐Ÿ™‹', '๐Ÿคฆ', '๐Ÿคท', '๐Ÿ™Ž', '๐Ÿ™', '๐Ÿ’‡', '๐Ÿ’†', '๐Ÿ•ด', '๐Ÿ’ƒ', '๐Ÿ•บ', '๐Ÿšถ', '๐Ÿƒ', '๐Ÿคฒ', '๐Ÿ‘', '๐Ÿ™Œ', '๐Ÿ‘', '๐Ÿค', '๐Ÿ‘', '๐Ÿ‘Ž', '๐Ÿ‘Š', 'โœŠ', '๐Ÿค›', '๐Ÿคœ', '๐Ÿคž', 'โœŒ๏ธ', '๐ŸคŸ', '๐Ÿค˜', '๐Ÿ‘Œ', '๐Ÿ‘ˆ', '๐Ÿ‘‰', '๐Ÿ‘†', '๐Ÿ‘‡', 'โ˜๏ธ', 'โœ‹', '๐Ÿคš', '๐Ÿ–', '๐Ÿ––', '๐Ÿ‘‹', '๐Ÿค™', '๐Ÿ’ช', '๐Ÿ–•', 'โœ๏ธ', '๐Ÿคณ', '๐Ÿ’…', '๐Ÿ‘‚', '๐Ÿ‘ƒ', '๐Ÿ‘ฃ', '๐Ÿ‘€', '๐Ÿ‘', '๐Ÿง ', '๐Ÿ‘…', '๐Ÿ‘„', '๐Ÿ’‹', '๐Ÿ‘“', '๐Ÿ•ถ', '๐Ÿ‘”', '๐Ÿ‘•', '๐Ÿ‘–', '๐Ÿงฃ', '๐Ÿงค', '๐Ÿงฅ', '๐Ÿงฆ', '๐Ÿ‘—', '๐Ÿ‘˜', '๐Ÿ‘™', '๐Ÿ‘š', '๐Ÿ‘›', '๐Ÿ‘œ', '๐Ÿ‘', '๐Ÿ›', '๐ŸŽ’', '๐Ÿ‘ž', '๐Ÿ‘Ÿ', '๐Ÿ‘ ', '๐Ÿ‘ก', '๐Ÿ‘ข', '๐Ÿ‘‘', '๐Ÿ‘’', '๐ŸŽฉ', '๐ŸŽ“', '๐Ÿงข', 'โ›‘', '๐Ÿ“ฟ', '๐Ÿ’„', '๐Ÿ’', '๐Ÿ’Ž', '๐Ÿ”‡', '๐Ÿ”ˆ', '๐Ÿ”‰', '๐Ÿ”Š', '๐Ÿ“ข', '๐Ÿ“ฃ', '๐Ÿ“ฏ', '๐Ÿ””', '๐Ÿ”•', '๐ŸŽผ', '๐ŸŽต', '๐ŸŽถ', '๐ŸŽ™', '๐ŸŽš', '๐ŸŽ›', '๐ŸŽค', '๐ŸŽง', '๐Ÿ“ป', '๐ŸŽท', '๐ŸŽธ', '๐ŸŽน', '๐ŸŽบ', '๐ŸŽป', '๐Ÿฅ', '๐Ÿ“ฑ', '๐Ÿ“ฒ', '๐Ÿ’ป', '๐Ÿ–ฅ', '๐Ÿ–จ', '๐Ÿ–ฑ', '๐Ÿ–ฒ', '๐Ÿ•น', '๐Ÿ—œ', '๐Ÿ’ฝ', '๐Ÿ’พ', '๐Ÿ’ฟ', '๐Ÿ“€', '๐Ÿ“ผ', '๐Ÿ“ท', '๐Ÿ“ธ', '๐Ÿ“น', '๐ŸŽฅ', '๐Ÿ“ฝ', '๐ŸŽž', '๐Ÿ“ž', 'โ˜Ž๏ธ', '๐Ÿ“Ÿ', '๐Ÿ“ ', '๐Ÿ“บ', '๐Ÿ“ป', '๐ŸŽ™', '๐Ÿ“ก', '๐Ÿ”', '๐Ÿ”Ž', '๐Ÿ”ฌ', '๐Ÿ”ญ', '๐Ÿ“ก', '๐Ÿ’ก', '๐Ÿ”ฆ', '๐Ÿฎ', '๐Ÿ“”', '๐Ÿ“•', '๐Ÿ“–', '๐Ÿ“—', '๐Ÿ“˜', '๐Ÿ“™', '๐Ÿ“š', '๐Ÿ““', '๐Ÿ“’', '๐Ÿ“ƒ', '๐Ÿ“œ', '๐Ÿ“„', '๐Ÿ“ฐ', '๐Ÿ—ž', '๐Ÿ“‘', '๐Ÿ”–', '๐Ÿท', '๐Ÿ’ฐ', '๐Ÿ’ด', '๐Ÿ’ต', '๐Ÿ’ถ', '๐Ÿ’ท', '๐Ÿ’ธ', '๐Ÿ’ณ', '๐Ÿงพ', '๐Ÿ’น', '๐Ÿ’ฑ', '๐Ÿ’ฒ', 'โœ‰๏ธ', '๐Ÿ“ง', '๐Ÿ“จ', '๐Ÿ“ฉ', '๐Ÿ“ค', '๐Ÿ“ฅ', '๐Ÿ“ฆ', '๐Ÿ“ซ', '๐Ÿ“ช', '๐Ÿ“ฌ', '๐Ÿ“ญ', '๐Ÿ“ฎ', '๐Ÿ—ณ', 'โœ๏ธ', 'โœ’๏ธ', '๐Ÿ–‹', '๐Ÿ–Š', '๐Ÿ–Œ', '๐Ÿ–', '๐Ÿ“', '๐Ÿ’ผ', '๐Ÿ“', '๐Ÿ“‚', '๐Ÿ—‚', '๐Ÿ“…', '๐Ÿ“†', '๐Ÿ—’', '๐Ÿ—“', '๐Ÿ“‡', '๐Ÿ“ˆ', '๐Ÿ“‰', '๐Ÿ“Š', '๐Ÿ“‹', '๐Ÿ“Œ', '๐Ÿ“', '๐Ÿ“Ž', '๐Ÿ–‡', '๐Ÿ“', '๐Ÿ“', 'โœ‚๏ธ', '๐Ÿ—ƒ', '๐Ÿ—„', '๐Ÿ—‘', '๐Ÿ”’', '๐Ÿ”“', '๐Ÿ”', '๐Ÿ”', '๐Ÿ”‘', '๐Ÿ—', '๐Ÿ”จ', 'โ›', 'โš’', '๐Ÿ› ', '๐Ÿ—ก', 'โš”๏ธ', '๐Ÿ”ซ', '๐Ÿน', '๐Ÿ›ก', '๐Ÿ”ง', '๐Ÿ”ฉ', 'โš™๏ธ', '๐Ÿ—œ', 'โš–๏ธ', '๐Ÿ”—', 'โ›“', '๐Ÿงฐ', '๐Ÿงฒ', 'โš—๏ธ', '๐Ÿงช', '๐Ÿงซ', '๐Ÿงฌ', '๐Ÿ”ฌ', '๐Ÿ”ญ', '๐Ÿ“ก', '๐Ÿ’‰', '๐Ÿ’Š', '๐Ÿ›', '๐Ÿ›‹', '๐Ÿšช', '๐Ÿšฝ', '๐Ÿšฟ', '๐Ÿ›', '๐Ÿงด', '๐Ÿงท', '๐Ÿงน', '๐Ÿงบ', '๐Ÿงป', '๐Ÿงผ', '๐Ÿงฝ', '๐Ÿงฏ', '๐Ÿšฌ', 'โšฐ๏ธ', 'โšฑ๏ธ', '๐Ÿ—ฟ', '๐Ÿบ', '๐Ÿงฑ', '๐ŸŽˆ', '๐ŸŽ', '๐ŸŽ€', '๐ŸŽ', '๐ŸŽŠ', '๐ŸŽ‰', '๐ŸŽŽ', '๐Ÿฎ', '๐ŸŽ', '๐Ÿงง', 'โœ‰๏ธ', '๐Ÿ“ฉ', '๐Ÿ“จ', '๐Ÿ“ง'];
251
+ let num = integer(1, max);
252
+ let arr = [];
253
+ for (let i = 0; i < num; i++) {
254
+ arr.push(chance.pickone(emojis));
255
+ }
256
+ if (array) return arr;
257
+ if (!array) return arr.join(', ');
258
+ };
259
+ }
217
260
 
218
261
  module.exports = {
219
262
  weightedRange,
@@ -230,4 +273,6 @@ module.exports = {
230
273
  openFinder,
231
274
  applySkew,
232
275
  boxMullerRandom,
276
+ generateEmoji,
277
+ getUniqueKeys
233
278
  };