@unchainedshop/core-assortments 1.1.3

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.
@@ -0,0 +1,438 @@
1
+ import { ModuleInput, ModuleMutations, Query, Tree } from '@unchainedshop/types/common';
2
+ import {
3
+ AssortmentsModule,
4
+ Assortment,
5
+ AssortmentLink,
6
+ AssortmentQuery,
7
+ AssortmentsSettingsOptions,
8
+ } from '@unchainedshop/types/assortments';
9
+ import { emit, registerEvents } from '@unchainedshop/events';
10
+ import { log, LogLevel } from '@unchainedshop/logger';
11
+ import {
12
+ generateDbMutations,
13
+ generateDbFilterById,
14
+ findPreservingIds,
15
+ buildSortOptions,
16
+ } from '@unchainedshop/utils';
17
+ import { SortDirection, SortOption } from '@unchainedshop/types/api';
18
+ import { resolveAssortmentProductFromDatabase } from '../utils/breadcrumbs/resolveAssortmentProductFromDatabase';
19
+ import { resolveAssortmentLinkFromDatabase } from '../utils/breadcrumbs/resolveAssortmentLinkFromDatabase';
20
+ import addMigrations from '../migrations/addMigrations';
21
+ import { AssortmentsCollection } from '../db/AssortmentsCollection';
22
+ import { AssortmentsSchema } from '../db/AssortmentsSchema';
23
+ import { configureAssortmentFiltersModule } from './configureAssortmentFiltersModule';
24
+ import { configureAssortmentLinksModule } from './configureAssortmentLinksModule';
25
+ import { assortmentsSettings } from '../assortments-settings';
26
+ import { configureAssortmentProductsModule } from './configureAssortmentProductsModule';
27
+ import { configureAssortmentTextsModule } from './configureAssortmentTextsModule';
28
+ import { configureAssortmentMediaModule } from './configureAssortmentMediaModule';
29
+ import { makeAssortmentBreadcrumbsBuilder } from '../utils/breadcrumbs/makeAssortmentBreadcrumbsBuilder';
30
+
31
+ const ASSORTMENT_EVENTS = [
32
+ 'ASSORTMENT_CREATE',
33
+ 'ASSORTMENT_REMOVE',
34
+ 'ASSORTMENT_SET_BASE',
35
+ 'ASSORTMENT_UPDATE',
36
+ ];
37
+
38
+ const buildFindSelector = ({
39
+ assortmentIds,
40
+ assortmentSelector,
41
+ slugs,
42
+ tags,
43
+ includeLeaves = false,
44
+ includeInactive = false,
45
+ queryString,
46
+ }: AssortmentQuery) => {
47
+ const selector: Query = assortmentSelector || {};
48
+
49
+ if (assortmentIds) {
50
+ selector._id = { $in: assortmentIds };
51
+ }
52
+
53
+ if (slugs) {
54
+ selector.slugs = { $in: slugs };
55
+ }
56
+
57
+ if (tags) {
58
+ if (Array.isArray(tags)) {
59
+ selector.tags = { $all: tags };
60
+ } else {
61
+ selector.tags = tags;
62
+ }
63
+ }
64
+
65
+ if (!assortmentSelector && !includeLeaves) {
66
+ selector.isRoot = true;
67
+ }
68
+
69
+ if (queryString) {
70
+ selector.$text = { $search: queryString };
71
+ }
72
+
73
+ if (!assortmentSelector && !includeInactive) {
74
+ selector.isActive = true;
75
+ }
76
+ return selector;
77
+ };
78
+
79
+ export const configureAssortmentsModule = async ({
80
+ db,
81
+ migrationRepository,
82
+ options: assortmentOptions = {},
83
+ }: ModuleInput<AssortmentsSettingsOptions>): Promise<AssortmentsModule> => {
84
+ // Events
85
+ registerEvents(ASSORTMENT_EVENTS);
86
+
87
+ // Settings
88
+ await assortmentsSettings.configureSettings(assortmentOptions, db);
89
+
90
+ // Migration
91
+ addMigrations(migrationRepository);
92
+
93
+ // Collections & Mutations
94
+ const { Assortments, AssortmentTexts, AssortmentProducts, AssortmentLinks, AssortmentFilters } =
95
+ await AssortmentsCollection(db);
96
+
97
+ const mutations = generateDbMutations<Assortment>(
98
+ Assortments,
99
+ AssortmentsSchema,
100
+ ) as ModuleMutations<Assortment>;
101
+
102
+ // Functions
103
+ const findLinkedAssortments = async (assortment: Assortment): Promise<Array<AssortmentLink>> => {
104
+ return AssortmentLinks.find(
105
+ {
106
+ $or: [{ parentAssortmentId: assortment._id }, { childAssortmentId: assortment._id }],
107
+ },
108
+ {
109
+ sort: { sortKey: 1 },
110
+ },
111
+ ).toArray();
112
+ };
113
+
114
+ const findProductAssignments = async (assortmentId: string) => {
115
+ return AssortmentProducts.find(
116
+ { assortmentId },
117
+ {
118
+ sort: { sortKey: 1 },
119
+ },
120
+ ).toArray();
121
+ };
122
+
123
+ // returns AssortmentProducts and child assortment links with products.
124
+ const collectProductIdCacheTree = async (assortment: Assortment): Promise<Tree<string>> => {
125
+ // get assortment products related with this assortment I.E AssortmentProducts
126
+ const productAssignments = await findProductAssignments(assortment._id);
127
+ const ownProductIds = productAssignments.map(({ productId }) => productId);
128
+
129
+ // get assortment links parent or child linked with this assortment I.E. AssortmentLinks
130
+ const linkedAssortments = await findLinkedAssortments(assortment);
131
+
132
+ // filter previous result set to get child assortment links
133
+ const childAssortments = linkedAssortments.filter(
134
+ ({ parentAssortmentId }) => parentAssortmentId === assortment._id,
135
+ );
136
+
137
+ // perform the whole function recursively for each child
138
+ const productIds = await Promise.all(
139
+ childAssortments.map(async ({ childAssortmentId }) => {
140
+ const childAssortment = await Assortments.findOne(
141
+ generateDbFilterById(childAssortmentId, { isActive: true }),
142
+ {},
143
+ );
144
+
145
+ if (childAssortment) {
146
+ return collectProductIdCacheTree(childAssortment);
147
+ }
148
+ return [];
149
+ }),
150
+ );
151
+
152
+ return [...ownProductIds, ...productIds];
153
+ };
154
+
155
+ const buildProductIds = async (assortment: Assortment) => {
156
+ const collectedProductIdTree = (await collectProductIdCacheTree(assortment)) || [];
157
+ const assortmentSet = new Set<string>(assortmentsSettings.zipTree(collectedProductIdTree));
158
+ return [...assortmentSet];
159
+ };
160
+
161
+ const findProductIds = async (
162
+ assortment: Assortment,
163
+ { forceLiveCollection = false, ignoreChildAssortments = false } = {},
164
+ ) => {
165
+ if (ignoreChildAssortments) {
166
+ const productAssignments = await findProductAssignments(assortment._id);
167
+ return productAssignments.map(({ productId }) => productId);
168
+ }
169
+ if (!forceLiveCollection) {
170
+ const cachedProductIds = await assortmentsSettings.getCachedProductIds(assortment._id);
171
+ if (cachedProductIds) return cachedProductIds;
172
+ }
173
+ return buildProductIds(assortment);
174
+ };
175
+
176
+ const invalidateProductIdCache = async (
177
+ assortment: Assortment,
178
+ cacheOptions: { skipUpstreamTraversal: boolean } = {
179
+ skipUpstreamTraversal: false,
180
+ },
181
+ ) => {
182
+ const productIds = await buildProductIds(assortment);
183
+
184
+ let updateCount = await assortmentsSettings.setCachedProductIds(assortment._id, productIds);
185
+
186
+ if (cacheOptions.skipUpstreamTraversal) return updateCount;
187
+
188
+ const linkedAssortments = await findLinkedAssortments(assortment);
189
+
190
+ const filteredLinkedAssortments = linkedAssortments.filter(
191
+ ({ childAssortmentId }) => childAssortmentId === assortment._id,
192
+ );
193
+
194
+ await Promise.all(
195
+ filteredLinkedAssortments.map(async ({ parentAssortmentId }) => {
196
+ const parent = await Assortments.findOne(generateDbFilterById(parentAssortmentId), {});
197
+
198
+ if (parent) {
199
+ updateCount += await invalidateProductIdCache(parent, cacheOptions);
200
+ }
201
+ return true;
202
+ }),
203
+ );
204
+
205
+ return updateCount;
206
+ };
207
+
208
+ const invalidateCache: AssortmentsModule['invalidateCache'] = async (selector, options) => {
209
+ log('Assortments: Start invalidating assortment caches', {
210
+ level: LogLevel.Verbose,
211
+ });
212
+
213
+ const assortments = await Assortments.find(
214
+ buildFindSelector({ includeInactive: true, includeLeaves: true, ...selector }),
215
+ ).toArray();
216
+
217
+ await Promise.all(
218
+ assortments.map(async (assortment) => {
219
+ await invalidateProductIdCache(assortment, {
220
+ skipUpstreamTraversal: options?.skipUpstreamTraversal ?? true,
221
+ });
222
+ }),
223
+ );
224
+ };
225
+
226
+ /*
227
+ * Assortment sub entities
228
+ */
229
+
230
+ const assortmentFilters = configureAssortmentFiltersModule({
231
+ AssortmentFilters,
232
+ });
233
+ const assortmentLinks = configureAssortmentLinksModule({
234
+ AssortmentLinks,
235
+ invalidateCache,
236
+ });
237
+ const assortmentProducts = configureAssortmentProductsModule({
238
+ AssortmentProducts,
239
+ invalidateCache,
240
+ });
241
+
242
+ const assortmentTexts = configureAssortmentTextsModule({
243
+ Assortments,
244
+ AssortmentTexts,
245
+ });
246
+
247
+ /*
248
+ * Assortment Module
249
+ */
250
+
251
+ return {
252
+ // Queries
253
+ findAssortment: async ({ assortmentId, slug }) => {
254
+ let selector: Query = {};
255
+
256
+ if (assortmentId) {
257
+ selector = generateDbFilterById(assortmentId);
258
+ } else if (slug) {
259
+ selector.slugs = slug;
260
+ } else {
261
+ return null;
262
+ }
263
+
264
+ return Assortments.findOne(selector, {});
265
+ },
266
+
267
+ findAssortments: async ({ limit, offset, sort, ...query }) => {
268
+ const defaultSortOption: Array<SortOption> = [{ key: 'sequence', value: SortDirection.ASC }];
269
+ const assortments = Assortments.find(buildFindSelector(query), {
270
+ skip: offset,
271
+ limit,
272
+ sort: buildSortOptions(sort || defaultSortOption),
273
+ });
274
+ return assortments.toArray();
275
+ },
276
+
277
+ findProductIds: async ({ assortmentId, forceLiveCollection, ignoreChildAssortments }) => {
278
+ const assortment = await Assortments.findOne(generateDbFilterById(assortmentId), {});
279
+ return findProductIds(assortment, {
280
+ forceLiveCollection,
281
+ ignoreChildAssortments,
282
+ });
283
+ },
284
+
285
+ children: async ({ assortmentId, includeInactive }) => {
286
+ const links = await AssortmentLinks.find(
287
+ { parentAssortmentId: assortmentId },
288
+ {
289
+ projection: { childAssortmentId: 1 },
290
+ sort: { sortKey: 1 },
291
+ },
292
+ ).toArray();
293
+
294
+ const assortmentIds = links.map(({ childAssortmentId }) => childAssortmentId);
295
+
296
+ const selector = !includeInactive ? { isActive: true } : {};
297
+ return findPreservingIds(Assortments)(selector, assortmentIds);
298
+ },
299
+
300
+ count: async (query) => Assortments.countDocuments(buildFindSelector(query)),
301
+
302
+ assortmentExists: async ({ assortmentId }) => {
303
+ const assortmentCount = await Assortments.countDocuments(generateDbFilterById(assortmentId), {
304
+ limit: 1,
305
+ });
306
+ return !!assortmentCount;
307
+ },
308
+
309
+ breadcrumbs: async (params) => {
310
+ const resolveAssortmentLink = resolveAssortmentLinkFromDatabase(AssortmentLinks);
311
+ const resolveAssortmentProducts = resolveAssortmentProductFromDatabase(AssortmentProducts);
312
+
313
+ const buildBreadcrumbs = makeAssortmentBreadcrumbsBuilder({
314
+ resolveAssortmentLink,
315
+ resolveAssortmentProducts,
316
+ });
317
+
318
+ return buildBreadcrumbs(params);
319
+ },
320
+
321
+ // Mutations
322
+ create: async (
323
+ {
324
+ authorId,
325
+ isActive = true,
326
+ isBase = false,
327
+ isRoot = false,
328
+ locale,
329
+ meta = {},
330
+ sequence,
331
+ title,
332
+ ...rest
333
+ },
334
+ userId,
335
+ ) => {
336
+ const assortmentId = await mutations.create(
337
+ {
338
+ sequence: sequence || (await Assortments.countDocuments({})) + 10,
339
+ isBase,
340
+ isActive,
341
+ isRoot,
342
+ meta,
343
+ authorId,
344
+ ...rest,
345
+ },
346
+ userId,
347
+ );
348
+
349
+ if (locale) {
350
+ await assortmentTexts.upsertLocalizedText(assortmentId, locale, { title }, userId);
351
+ }
352
+
353
+ const assortment = await Assortments.findOne(generateDbFilterById(assortmentId));
354
+ emit('ASSORTMENT_CREATE', { assortment });
355
+ return assortmentId;
356
+ },
357
+
358
+ update: async (_id, doc, userId, options) => {
359
+ const assortmentId = await mutations.update(_id, doc, userId);
360
+ emit('ASSORTMENT_UPDATE', { assortmentId });
361
+
362
+ if (!options?.skipInvalidation) {
363
+ const assortment = await Assortments.findOne({ _id: assortmentId });
364
+ await invalidateProductIdCache(assortment, { skipUpstreamTraversal: false });
365
+ }
366
+ return assortmentId;
367
+ },
368
+
369
+ delete: async (assortmentId, options, userId) => {
370
+ await assortmentLinks.deleteMany(
371
+ {
372
+ $or: [{ parentAssortmentId: assortmentId }, { childAssortmentId: assortmentId }],
373
+ },
374
+ { skipInvalidation: true },
375
+ userId,
376
+ );
377
+
378
+ await assortmentProducts.deleteMany({ assortmentId }, { skipInvalidation: true }, userId);
379
+
380
+ await assortmentFilters.deleteMany({ assortmentId }, userId);
381
+
382
+ await assortmentTexts.deleteMany({ assortmentId }, userId);
383
+
384
+ const deletedResult = await Assortments.deleteOne(generateDbFilterById(assortmentId));
385
+
386
+ if (deletedResult.deletedCount === 1 && !options?.skipInvalidation) {
387
+ // Invalidate all assortments
388
+ await invalidateCache({});
389
+ }
390
+
391
+ emit('ASSORTMENT_REMOVE', { assortmentId });
392
+
393
+ return deletedResult.deletedCount;
394
+ },
395
+
396
+ invalidateCache,
397
+ setBase: async (assortmentId, userId) => {
398
+ await Assortments.updateMany(
399
+ { isBase: true },
400
+ {
401
+ $set: {
402
+ isBase: false,
403
+ updated: new Date(),
404
+ updatedBy: userId,
405
+ },
406
+ },
407
+ );
408
+
409
+ await Assortments.updateOne(generateDbFilterById(assortmentId), {
410
+ $set: {
411
+ isBase: true,
412
+ updated: new Date(),
413
+ updatedBy: userId,
414
+ },
415
+ });
416
+ emit('ASSORTMENT_SET_BASE', { assortmentId });
417
+ },
418
+
419
+ search: {
420
+ findFilteredAssortments: async ({ limit, offset, assortmentIds, assortmentSelector, sort }) => {
421
+ const assortments = await findPreservingIds(Assortments)(assortmentSelector, assortmentIds, {
422
+ limit,
423
+ skip: offset,
424
+ sort,
425
+ });
426
+
427
+ return assortments;
428
+ },
429
+ },
430
+
431
+ // Sub entities
432
+ media: await configureAssortmentMediaModule({ db }),
433
+ filters: assortmentFilters,
434
+ links: assortmentLinks,
435
+ products: assortmentProducts,
436
+ texts: assortmentTexts,
437
+ };
438
+ };
@@ -0,0 +1,40 @@
1
+ import { Db } from '@unchainedshop/types/common';
2
+ import { AssortmentsCollection } from '../db/AssortmentsCollection';
3
+
4
+ const eqSet = (as, bs) => {
5
+ return [...as].join(',') === [...bs].join(',');
6
+ };
7
+
8
+ export default async function mongodbCache(db: Db) {
9
+ const { AssortmentProductIdCache } = await AssortmentsCollection(db);
10
+
11
+ return {
12
+ async getCachedProductIds(assortmentId) {
13
+ const assortmentProductIdCache = await AssortmentProductIdCache.findOne({
14
+ _id: assortmentId,
15
+ });
16
+ return assortmentProductIdCache?.productIds;
17
+ },
18
+ async setCachedProductIds(assortmentId, productIds) {
19
+ const assortmentProductIdCache = await AssortmentProductIdCache.findOne({
20
+ _id: assortmentId,
21
+ });
22
+ if (
23
+ assortmentProductIdCache &&
24
+ eqSet(new Set(productIds), new Set(assortmentProductIdCache.productIds))
25
+ ) {
26
+ return 0;
27
+ }
28
+ const updateResult = await AssortmentProductIdCache.updateOne(
29
+ { _id: assortmentId },
30
+ {
31
+ $set: {
32
+ productIds,
33
+ },
34
+ },
35
+ { upsert: true },
36
+ );
37
+ return updateResult.modifiedCount;
38
+ },
39
+ };
40
+ }
@@ -0,0 +1,58 @@
1
+ const walkAssortmentLinks = (resolveAssortmentLink) => async (rootAssortmentId) => {
2
+ const walk = async (assortmentId, initialPaths: string[], childAssortmentId?: string) => {
3
+ const assortmentLink = await resolveAssortmentLink(assortmentId, childAssortmentId);
4
+ if (!assortmentLink) return initialPaths;
5
+
6
+ const subAsssortmentLinks = await Promise.all(
7
+ assortmentLink.parentIds.map(async (parentAssortmentId) => {
8
+ return walk(parentAssortmentId, initialPaths, assortmentId);
9
+ }),
10
+ );
11
+
12
+ if (subAsssortmentLinks.length > 0) {
13
+ return subAsssortmentLinks
14
+ .map((subAsssortmentLink) => {
15
+ return subAsssortmentLink.map((subSubLinks) => [
16
+ ...subSubLinks,
17
+ assortmentLink,
18
+ ...initialPaths,
19
+ ]);
20
+ })
21
+ .flat();
22
+ }
23
+ return [[assortmentLink, ...initialPaths]];
24
+ };
25
+ // Recursively walk up the directed graph in reverse
26
+ return walk(rootAssortmentId, []);
27
+ };
28
+
29
+ export const walkUpFromProduct = async ({
30
+ resolveAssortmentProducts,
31
+ resolveAssortmentLink,
32
+ productId,
33
+ }) => {
34
+ const pathResolver = walkAssortmentLinks(resolveAssortmentLink);
35
+ const assortmentProducts = await resolveAssortmentProducts(productId);
36
+ return (
37
+ await Promise.all(
38
+ assortmentProducts.map(async (assortmentProduct) => {
39
+ // Walk up the assortments to find all distinct paths
40
+ const paths = await pathResolver(assortmentProduct.assortmentId);
41
+ return paths.map((links) => ({
42
+ ...assortmentProduct,
43
+ links,
44
+ }));
45
+ }),
46
+ )
47
+ ).flat();
48
+ };
49
+
50
+ export const walkUpFromAssortment = async ({ resolveAssortmentLink, assortmentId }) => {
51
+ const pathResolver = walkAssortmentLinks(resolveAssortmentLink);
52
+ const paths = await pathResolver(assortmentId);
53
+ return paths
54
+ .map((links) => ({
55
+ links: links.slice(0, -1),
56
+ }))
57
+ .filter(({ links }) => links.length);
58
+ };
@@ -0,0 +1,20 @@
1
+ import { walkUpFromAssortment, walkUpFromProduct } from './build-paths';
2
+
3
+ export const buildBreadcrumbs = async (params) => {
4
+ const { productId } = params;
5
+ if (productId) return walkUpFromProduct(params);
6
+ return walkUpFromAssortment(params);
7
+ };
8
+
9
+ export const makeAssortmentBreadcrumbsBuilder = ({
10
+ resolveAssortmentLink,
11
+ resolveAssortmentProducts,
12
+ }) => {
13
+ return async (params: { assortmentId?: string; productId?: string }) =>
14
+ buildBreadcrumbs({
15
+ assortmentId: params.assortmentId,
16
+ productId: params.productId,
17
+ resolveAssortmentLink,
18
+ resolveAssortmentProducts,
19
+ });
20
+ };
@@ -0,0 +1,25 @@
1
+ import { AssortmentLink } from '@unchainedshop/types/assortments';
2
+ import type { Collection, QuerySelector } from 'mongodb';
3
+
4
+ export function resolveAssortmentLinkFromDatabase(
5
+ AssortmentLinks: Collection<AssortmentLink>,
6
+ selector: QuerySelector<AssortmentLink> = {},
7
+ ) {
8
+ return async (assortmentId: string, childAssortmentId: string) => {
9
+ const links = await AssortmentLinks.find(
10
+ { childAssortmentId: assortmentId, ...selector },
11
+ {
12
+ projection: { childAssortmentId: 1, parentAssortmentId: 1 },
13
+ sort: { sortKey: 1, parentAssortmentId: 1 },
14
+ },
15
+ ).toArray();
16
+
17
+ const parentIds = links.map((link) => link.parentAssortmentId);
18
+
19
+ return {
20
+ assortmentId,
21
+ childAssortmentId,
22
+ parentIds,
23
+ };
24
+ };
25
+ }
@@ -0,0 +1,19 @@
1
+ import { AssortmentProduct } from '@unchainedshop/types/assortments';
2
+ import type { Collection, QuerySelector } from 'mongodb';
3
+
4
+ export function resolveAssortmentProductFromDatabase(
5
+ AssortmentProducts: Collection<AssortmentProduct>,
6
+ selector: QuerySelector<AssortmentProduct> = {},
7
+ ) {
8
+ return async (productId: string) => {
9
+ const products = AssortmentProducts.find(
10
+ { productId, ...selector },
11
+ {
12
+ projection: { _id: true, assortmentId: true },
13
+ sort: { sortKey: 1, productId: 1 },
14
+ },
15
+ );
16
+
17
+ return products.toArray();
18
+ };
19
+ }
@@ -0,0 +1,60 @@
1
+ import { Tree } from '@unchainedshop/types/common';
2
+ import * as R from 'ramda';
3
+
4
+ const fillUp = <T>(arr: Array<T>, size: number): Array<T> =>
5
+ [...arr, ...new Array(size).fill(null)].slice(0, size);
6
+
7
+ const fillToSameLengthArray = <T>(a: Array<T>, b: Array<T>) => {
8
+ const length = Math.max(a.length, b.length);
9
+ return [fillUp(a, length), fillUp(b, length)];
10
+ };
11
+
12
+ const divideTreeByLevels = (
13
+ array: Tree<string>,
14
+ level = 0,
15
+ ): Array<{ level: number; items: Array<string> }> => {
16
+ const currentLevel: Array<string> = array.reduce((acc, item) => {
17
+ if (typeof item === 'object') {
18
+ return acc;
19
+ }
20
+ return [...acc, item];
21
+ }, []) as Array<string>;
22
+
23
+ const nextLevels = array.reduce((acc, item) => {
24
+ if (typeof item === 'object') {
25
+ return [...acc, ...divideTreeByLevels(item, level + 1)];
26
+ }
27
+ return acc;
28
+ }, []) as Array<{ level: number; items: Array<string> }>;
29
+
30
+ return [currentLevel.length && { level, items: currentLevel }, ...nextLevels].filter(Boolean);
31
+ };
32
+
33
+ const concatItemsByLevels = (levelArray): Tree<string> => {
34
+ return Object.values(
35
+ levelArray.reduce((acc, { level, items }) => {
36
+ return {
37
+ ...acc,
38
+ [level]: [...(acc[level] || []), items],
39
+ };
40
+ }, {}),
41
+ );
42
+ };
43
+
44
+ const shuffleEachLevel = (unshuffledLevels) => {
45
+ return unshuffledLevels.map((subArrays) => {
46
+ const shuffled = subArrays.reduce((a, b) => {
47
+ const [accumulator, currentArray] = fillToSameLengthArray(a, b);
48
+ return R.zip(accumulator, currentArray);
49
+ }, []);
50
+ return shuffled;
51
+ });
52
+ };
53
+
54
+ export default (tree: Tree<string>): Array<string> => {
55
+ const levels = divideTreeByLevels(tree);
56
+ const concattedLevels = concatItemsByLevels(levels);
57
+ const items = shuffleEachLevel(concattedLevels);
58
+ const zipped: Array<string> = R.pipe(R.flatten, R.filter(Boolean))(items);
59
+ return zipped;
60
+ };
@@ -0,0 +1,7 @@
1
+ import * as R from 'ramda';
2
+ import { Tree } from '@unchainedshop/types/common';
3
+
4
+ export default (tree: Tree<string>): Array<string> => {
5
+ const zipped = R.pipe(R.flatten, R.filter(Boolean))(tree);
6
+ return zipped;
7
+ };