@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.
- package/.npm/package/README +7 -0
- package/.npm/package/npm-shrinkwrap.json +200 -0
- package/.versions +53 -0
- package/README.md +3 -0
- package/package.js +36 -0
- package/package.json +46 -0
- package/src/assortments-index.ts +3 -0
- package/src/assortments-settings.ts +27 -0
- package/src/db/AssortmentMediasCollection.ts +26 -0
- package/src/db/AssortmentMediasSchema.js +31 -0
- package/src/db/AssortmentsCollection.ts +84 -0
- package/src/db/AssortmentsSchema.js +75 -0
- package/src/migrations/addMigrations.ts +41 -0
- package/src/module/configureAssortmentFiltersModule.ts +153 -0
- package/src/module/configureAssortmentLinksModule.ts +212 -0
- package/src/module/configureAssortmentMediaModule.ts +259 -0
- package/src/module/configureAssortmentProductsModule.ts +234 -0
- package/src/module/configureAssortmentTextsModule.ts +179 -0
- package/src/module/configureAssortmentsModule.ts +438 -0
- package/src/product-cache/mongodb.ts +40 -0
- package/src/utils/breadcrumbs/build-paths.ts +58 -0
- package/src/utils/breadcrumbs/makeAssortmentBreadcrumbsBuilder.ts +20 -0
- package/src/utils/breadcrumbs/resolveAssortmentLinkFromDatabase.ts +25 -0
- package/src/utils/breadcrumbs/resolveAssortmentProductFromDatabase.ts +19 -0
- package/src/utils/tree-zipper/zipTreeByDeepness.ts +60 -0
- package/src/utils/tree-zipper/zipTreeBySimplyFlattening.ts +7 -0
- package/tests/assortments-index.test.ts +39 -0
- package/tsconfig.build.json +26 -0
- package/tsconfig.json +8 -0
|
@@ -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
|
+
};
|