@unchainedshop/core-assortments 5.0.0-alpha.3 → 5.0.0-alpha.5

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.
@@ -37,18 +37,18 @@ export const AssortmentsCollection = async (db) => {
37
37
  { index: { locale: 1, assortmentId: 1 } },
38
38
  ]);
39
39
  await buildDbIndexes(AssortmentProducts, [
40
- { index: { assortmentId: 1 } },
40
+ { index: { assortmentId: 1, productId: 1 } },
41
41
  { index: { productId: 1 } },
42
42
  { index: { tags: 1 } },
43
43
  { index: { assortmentId: 1, sortKey: 1 } },
44
- ]);
44
+ ], { rebuild: false });
45
45
  await buildDbIndexes(AssortmentLinks, [
46
46
  { index: { parentAssortmentId: 1 } },
47
- { index: { childAssortmentId: 1 } },
47
+ { index: { childAssortmentId: 1, sortKey: 1, parentAssortmentId: 1 } },
48
48
  { index: { tags: 1 } },
49
49
  { index: { parentAssortmentId: 1, sortKey: 1 } },
50
50
  { index: { childAssortmentId: 1, parentAssortmentId: 1, sortKey: 1 } },
51
- ]);
51
+ ], { rebuild: false });
52
52
  await buildDbIndexes(AssortmentFilters, [
53
53
  { index: { assortmentId: 1 } },
54
54
  { index: { filterId: 1 } },
@@ -18,7 +18,7 @@ export const configureAssortmentFiltersModule = ({ AssortmentFilters, }) => {
18
18
  },
19
19
  findFilterIds: async ({ assortmentId }) => {
20
20
  const filters = AssortmentFilters.find({ assortmentId }, {
21
- sort: { sortKey: 1 },
21
+ sort: { sortKey: 1, _id: 1 },
22
22
  projection: { filterId: 1 },
23
23
  }).map((filter) => filter.filterId);
24
24
  return filters.toArray();
@@ -41,7 +41,7 @@ export const configureAssortmentFiltersModule = ({ AssortmentFilters, }) => {
41
41
  created: new Date(),
42
42
  };
43
43
  if (sortKey === undefined || sortKey === null) {
44
- const lastAssortmentFilter = (await AssortmentFilters.findOne({ assortmentId }, { sort: { sortKey: -1 } })) || { sortKey: 0 };
44
+ const lastAssortmentFilter = (await AssortmentFilters.findOne({ assortmentId }, { sort: { sortKey: -1, _id: -1 } })) || { sortKey: 0 };
45
45
  $setOnInsert.sortKey = lastAssortmentFilter.sortKey + 1;
46
46
  }
47
47
  else {
@@ -9,11 +9,13 @@ export declare const configureAssortmentLinksModule: ({ AssortmentLinks, invalid
9
9
  parentAssortmentId?: string;
10
10
  childAssortmentId?: string;
11
11
  }) => Promise<mongodb.WithId<AssortmentLink> | null>;
12
- findLinks: ({ assortmentId, assortmentIds, parentAssortmentId, parentAssortmentIds, }: {
12
+ findLinks: ({ assortmentId, assortmentIds, parentAssortmentId, parentAssortmentIds, childAssortmentId, childAssortmentIds, }: {
13
13
  assortmentId?: string;
14
14
  assortmentIds?: string[];
15
15
  parentAssortmentId?: string;
16
16
  parentAssortmentIds?: string[];
17
+ childAssortmentId?: string;
18
+ childAssortmentIds?: string[];
17
19
  }, options?: mongodb.FindOptions) => Promise<AssortmentLink[]>;
18
20
  create: (doc: Omit<AssortmentLink, "_id" | "created" | "sortKey"> & Pick<Partial<AssortmentLink>, "_id" | "created" | "sortKey">, options?: {
19
21
  skipInvalidation?: boolean;
@@ -15,19 +15,23 @@ export const configureAssortmentLinksModule = ({ AssortmentLinks, invalidateCach
15
15
  ? generateDbFilterById(assortmentLinkId)
16
16
  : { parentAssortmentId, childAssortmentId }, {});
17
17
  },
18
- findLinks: async ({ assortmentId, assortmentIds, parentAssortmentId, parentAssortmentIds, }, options) => {
19
- const selector = parentAssortmentId || parentAssortmentIds
20
- ? {
21
- parentAssortmentId: parentAssortmentId || { $in: parentAssortmentIds },
22
- }
23
- : {
24
- $or: [
25
- { parentAssortmentId: assortmentId || { $in: assortmentIds } },
26
- { childAssortmentId: assortmentId || { $in: assortmentIds } },
27
- ],
28
- };
18
+ findLinks: async ({ assortmentId, assortmentIds, parentAssortmentId, parentAssortmentIds, childAssortmentId, childAssortmentIds, }, options) => {
19
+ const selectors = [];
20
+ const parentIds = parentAssortmentId || (parentAssortmentIds && { $in: parentAssortmentIds });
21
+ const childIds = childAssortmentId || (childAssortmentIds && { $in: childAssortmentIds });
22
+ const bidirectionalIds = assortmentId || (assortmentIds && { $in: assortmentIds });
23
+ if (parentIds)
24
+ selectors.push({ parentAssortmentId: parentIds });
25
+ if (childIds)
26
+ selectors.push({ childAssortmentId: childIds });
27
+ if (bidirectionalIds) {
28
+ selectors.push({ parentAssortmentId: bidirectionalIds }, { childAssortmentId: bidirectionalIds });
29
+ }
30
+ if (!selectors.length)
31
+ return [];
32
+ const selector = selectors.length > 1 ? { $or: selectors } : selectors[0];
29
33
  const links = AssortmentLinks.find(selector, options || {
30
- sort: { sortKey: 1 },
34
+ sort: { sortKey: 1, _id: 1 },
31
35
  });
32
36
  return links.toArray();
33
37
  },
@@ -37,7 +41,7 @@ export const configureAssortmentLinksModule = ({ AssortmentLinks, invalidateCach
37
41
  resolveAssortmentLinks: async (id) => {
38
42
  return AssortmentLinks.find({ childAssortmentId: id }, {
39
43
  projection: { _id: 1, childAssortmentId: 1, parentAssortmentId: 1 },
40
- sort: { sortKey: 1, parentAssortmentId: 1 },
44
+ sort: { sortKey: 1, parentAssortmentId: 1, _id: 1 },
41
45
  }).toArray();
42
46
  },
43
47
  assortmentId: parentAssortmentId,
@@ -56,7 +60,7 @@ export const configureAssortmentLinksModule = ({ AssortmentLinks, invalidateCach
56
60
  created: new Date(),
57
61
  };
58
62
  if (sortKey === undefined || sortKey === null) {
59
- const lastAssortmentLink = (await AssortmentLinks.findOne({ parentAssortmentId }, { sort: { sortKey: -1 } })) || { sortKey: 0 };
63
+ const lastAssortmentLink = (await AssortmentLinks.findOne({ parentAssortmentId }, { sort: { sortKey: -1, _id: -1 } })) || { sortKey: 0 };
60
64
  $setOnInsert.sortKey = lastAssortmentLink.sortKey + 1;
61
65
  }
62
66
  else {
@@ -54,7 +54,7 @@ export const configureAssortmentMediaModule = async ({ db }) => {
54
54
  const mediaList = AssortmentMedia.find(selector, {
55
55
  skip: offset,
56
56
  limit,
57
- sort: { sortKey: 1 },
57
+ sort: { sortKey: 1, _id: 1 },
58
58
  ...options,
59
59
  });
60
60
  return mediaList.toArray();
@@ -64,7 +64,7 @@ export const configureAssortmentMediaModule = async ({ db }) => {
64
64
  const lastAssortmentMedia = (await AssortmentMedia.findOne({
65
65
  assortmentId: doc.assortmentId,
66
66
  }, {
67
- sort: { sortKey: -1 },
67
+ sort: { sortKey: -1, _id: -1 },
68
68
  })) || { sortKey: 0 };
69
69
  sortKey = lastAssortmentMedia.sortKey + 1;
70
70
  }
@@ -25,7 +25,7 @@ export declare const configureAssortmentProductsModule: ({ AssortmentProducts, i
25
25
  productId: string;
26
26
  assortmentIds: string[];
27
27
  }) => Promise<string[]>;
28
- create: (doc: Omit<AssortmentProduct, "_id" | "created" | "sortKey"> & Partial<Pick<AssortmentProduct, "_id" | "created" | "sortKey">>, options?: {
28
+ create: (doc: Omit<AssortmentProduct, "_id" | "created" | "sortKey" | "tags"> & Partial<Pick<AssortmentProduct, "_id" | "created" | "sortKey" | "tags">>, options?: {
29
29
  skipInvalidation?: boolean;
30
30
  }) => Promise<mongodb.WithId<AssortmentProduct> | null>;
31
31
  delete: (assortmentProductId: string, options?: {
@@ -46,7 +46,7 @@ export const configureAssortmentProductsModule = ({ AssortmentProducts, invalida
46
46
  assortmentId: { $in: assortmentIds },
47
47
  productId: { $ne: productId },
48
48
  }, {
49
- sort: { sortKey: 1 },
49
+ sort: { sortKey: 1, _id: 1 },
50
50
  projection: { productId: 1 },
51
51
  }).toArray();
52
52
  return assortmentProducts.map((product) => product.productId);
@@ -69,7 +69,7 @@ export const configureAssortmentProductsModule = ({ AssortmentProducts, invalida
69
69
  created: new Date(),
70
70
  };
71
71
  if (sortKey === undefined || sortKey === null) {
72
- const lastAssortmentProduct = (await AssortmentProducts.findOne({ assortmentId }, { sort: { sortKey: -1 } })) || { sortKey: 0 };
72
+ const lastAssortmentProduct = (await AssortmentProducts.findOne({ assortmentId }, { sort: { sortKey: -1, _id: -1 } })) || { sortKey: 0 };
73
73
  $setOnInsert.sortKey = lastAssortmentProduct.sortKey + 1;
74
74
  }
75
75
  else {
@@ -52,6 +52,21 @@ export declare const configureAssortmentsModule: (moduleInput: ModuleInput<Assor
52
52
  skipInvalidation?: boolean;
53
53
  }) => Promise<mongodb.WithId<Assortment> | null>;
54
54
  invalidateCache: InvalidateCacheFn;
55
+ bulkDelete: (assortmentIds: string[]) => Promise<{
56
+ successIds: string[];
57
+ failedIds: string[];
58
+ }>;
59
+ bulkSetActive: (assortmentIds: string[], isActive: boolean) => Promise<{
60
+ successIds: string[];
61
+ failedIds: string[];
62
+ }>;
63
+ bulkUpdateTags: (assortmentIds: string[], { add, remove }: {
64
+ add?: string[];
65
+ remove?: string[];
66
+ }) => Promise<{
67
+ successIds: string[];
68
+ failedIds: string[];
69
+ }>;
55
70
  search: {
56
71
  findFilteredAssortments: ({ limit, offset, assortmentIds, assortmentSelector, sort, }: {
57
72
  assortmentIds: string[];
@@ -133,11 +148,13 @@ export declare const configureAssortmentsModule: (moduleInput: ModuleInput<Assor
133
148
  parentAssortmentId?: string;
134
149
  childAssortmentId?: string;
135
150
  }) => Promise<mongodb.WithId<AssortmentLink> | null>;
136
- findLinks: ({ assortmentId, assortmentIds, parentAssortmentId, parentAssortmentIds, }: {
151
+ findLinks: ({ assortmentId, assortmentIds, parentAssortmentId, parentAssortmentIds, childAssortmentId, childAssortmentIds, }: {
137
152
  assortmentId?: string;
138
153
  assortmentIds?: string[];
139
154
  parentAssortmentId?: string;
140
155
  parentAssortmentIds?: string[];
156
+ childAssortmentId?: string;
157
+ childAssortmentIds?: string[];
141
158
  }, options?: mongodb.FindOptions) => Promise<AssortmentLink[]>;
142
159
  create: (doc: Omit<AssortmentLink, "_id" | "created" | "sortKey"> & Pick<Partial<AssortmentLink>, "_id" | "created" | "sortKey">, options?: {
143
160
  skipInvalidation?: boolean;
@@ -182,7 +199,7 @@ export declare const configureAssortmentsModule: (moduleInput: ModuleInput<Assor
182
199
  productId: string;
183
200
  assortmentIds: string[];
184
201
  }) => Promise<string[]>;
185
- create: (doc: Omit<AssortmentProduct, "_id" | "created" | "sortKey"> & Partial<Pick<AssortmentProduct, "_id" | "created" | "sortKey">>, options?: {
202
+ create: (doc: Omit<AssortmentProduct, "_id" | "created" | "sortKey" | "tags"> & Partial<Pick<AssortmentProduct, "_id" | "created" | "sortKey" | "tags">>, options?: {
186
203
  skipInvalidation?: boolean;
187
204
  }) => Promise<mongodb.WithId<AssortmentProduct> | null>;
188
205
  delete: (assortmentProductId: string, options?: {
@@ -1,4 +1,4 @@
1
- import { SortDirection } from '@unchainedshop/utils';
1
+ import { executeBulkOperation, SortDirection } from '@unchainedshop/utils';
2
2
  import { emit, registerEvents } from '@unchainedshop/events';
3
3
  import { generateDbFilterById, findPreservingIds, buildSortOptions, mongodb, generateDbObjectId, } from '@unchainedshop/mongodb';
4
4
  import { createLogger } from '@unchainedshop/logger';
@@ -49,12 +49,12 @@ export const configureAssortmentsModule = async (moduleInput) => {
49
49
  return AssortmentLinks.find({
50
50
  $or: [{ parentAssortmentId: assortment._id }, { childAssortmentId: assortment._id }],
51
51
  }, {
52
- sort: { sortKey: 1 },
52
+ sort: { sortKey: 1, _id: 1 },
53
53
  }).toArray();
54
54
  };
55
55
  const findProductAssignments = async (assortmentId) => {
56
56
  return AssortmentProducts.find({ assortmentId }, {
57
- sort: { sortKey: 1 },
57
+ sort: { sortKey: 1, _id: 1 },
58
58
  }).toArray();
59
59
  };
60
60
  const collectProductIdCacheTree = async (assortment) => {
@@ -123,6 +123,42 @@ export const configureAssortmentsModule = async (moduleInput) => {
123
123
  AssortmentTexts,
124
124
  });
125
125
  const assortmentMedia = await configureAssortmentMediaModule(moduleInput);
126
+ const updateAssortment = async (assortmentId, doc, options) => {
127
+ const assortment = await Assortments.findOneAndUpdate(generateDbFilterById(assortmentId), {
128
+ $set: {
129
+ updated: new Date(),
130
+ ...doc,
131
+ },
132
+ }, { returnDocument: 'after' });
133
+ if (!assortment)
134
+ return null;
135
+ await emit('ASSORTMENT_UPDATE', { assortmentId });
136
+ if (!options?.skipInvalidation) {
137
+ await invalidateCache({ assortmentIds: [assortmentId] });
138
+ }
139
+ return assortment;
140
+ };
141
+ const deleteAssortment = async (assortmentId, options) => {
142
+ await assortmentLinks.deleteMany({
143
+ $or: [{ parentAssortmentId: assortmentId }, { childAssortmentId: assortmentId }],
144
+ }, { skipInvalidation: true });
145
+ await assortmentProducts.deleteMany({ assortmentId }, { skipInvalidation: true });
146
+ await assortmentFilters.deleteMany({ assortmentId });
147
+ await assortmentTexts.deleteMany({ assortmentId });
148
+ await assortmentMedia.deleteMediaFiles({ assortmentId });
149
+ const deletedAssortment = await Assortments.findOneAndUpdate(generateDbFilterById(assortmentId), {
150
+ $set: {
151
+ deleted: new Date(),
152
+ },
153
+ }, { returnDocument: 'after' });
154
+ if (!deletedAssortment)
155
+ return null;
156
+ if (!options?.skipInvalidation) {
157
+ await invalidateCache({}, { skipUpstreamTraversal: true });
158
+ }
159
+ await emit('ASSORTMENT_REMOVE', { assortmentId });
160
+ return deletedAssortment;
161
+ };
126
162
  return {
127
163
  findAssortment: async ({ assortmentId, slug }) => {
128
164
  let selector = {};
@@ -166,7 +202,7 @@ export const configureAssortmentsModule = async (moduleInput) => {
166
202
  children: async ({ assortmentId, includeInactive, }) => {
167
203
  const links = await AssortmentLinks.find({ parentAssortmentId: assortmentId }, {
168
204
  projection: { childAssortmentId: 1 },
169
- sort: { sortKey: 1 },
205
+ sort: { sortKey: 1, _id: 1 },
170
206
  }).toArray();
171
207
  const assortmentIds = links.map(({ childAssortmentId }) => childAssortmentId);
172
208
  const selector = !includeInactive ? { isActive: true } : {};
@@ -182,12 +218,12 @@ export const configureAssortmentsModule = async (moduleInput) => {
182
218
  breadcrumbs: async (params, { resolveAssortmentLinks = async (id) => {
183
219
  return AssortmentLinks.find({ childAssortmentId: id }, {
184
220
  projection: { _id: 1, childAssortmentId: 1, parentAssortmentId: 1 },
185
- sort: { sortKey: 1, parentAssortmentId: 1 },
221
+ sort: { sortKey: 1, parentAssortmentId: 1, _id: 1 },
186
222
  }).toArray();
187
223
  }, resolveAssortmentProducts = async (id) => {
188
224
  return AssortmentProducts.find({ productId: id }, {
189
225
  projection: { _id: true, assortmentId: true, productId: true },
190
- sort: { sortKey: 1, productId: 1 },
226
+ sort: { sortKey: 1, productId: 1, _id: 1 },
191
227
  }).toArray();
192
228
  }, }) => {
193
229
  const buildBreadcrumbs = makeAssortmentBreadcrumbsBuilder({
@@ -212,43 +248,39 @@ export const configureAssortmentsModule = async (moduleInput) => {
212
248
  await emit('ASSORTMENT_CREATE', { assortment });
213
249
  return assortment;
214
250
  },
215
- update: async (assortmentId, doc, options) => {
216
- const assortment = await Assortments.findOneAndUpdate(generateDbFilterById(assortmentId), {
217
- $set: {
218
- updated: new Date(),
219
- ...doc,
220
- },
221
- }, { returnDocument: 'after' });
222
- if (!assortment)
223
- return null;
224
- await emit('ASSORTMENT_UPDATE', { assortmentId });
225
- if (!options?.skipInvalidation) {
226
- await invalidateCache({ assortmentIds: [assortmentId] });
227
- }
228
- return assortment;
251
+ update: updateAssortment,
252
+ delete: deleteAssortment,
253
+ invalidateCache,
254
+ bulkDelete: async (assortmentIds) => {
255
+ return executeBulkOperation(assortmentIds, async (assortmentId) => {
256
+ const assortment = await Assortments.findOne({ _id: assortmentId, deleted: null }, {});
257
+ if (!assortment)
258
+ throw new Error('not-found');
259
+ if (!(await deleteAssortment(assortmentId))) {
260
+ throw new Error('delete-failed');
261
+ }
262
+ });
229
263
  },
230
- delete: async (assortmentId, options) => {
231
- await assortmentLinks.deleteMany({
232
- $or: [{ parentAssortmentId: assortmentId }, { childAssortmentId: assortmentId }],
233
- }, { skipInvalidation: true });
234
- await assortmentProducts.deleteMany({ assortmentId }, { skipInvalidation: true });
235
- await assortmentFilters.deleteMany({ assortmentId });
236
- await assortmentTexts.deleteMany({ assortmentId });
237
- await assortmentMedia.deleteMediaFiles({ assortmentId });
238
- const deletedAssortment = await Assortments.findOneAndUpdate(generateDbFilterById(assortmentId), {
239
- $set: {
240
- deleted: new Date(),
241
- },
242
- }, { returnDocument: 'after' });
243
- if (!deletedAssortment)
244
- return null;
245
- if (!options?.skipInvalidation) {
246
- await invalidateCache({}, { skipUpstreamTraversal: true });
247
- }
248
- await emit('ASSORTMENT_REMOVE', { assortmentId });
249
- return deletedAssortment;
264
+ bulkSetActive: async (assortmentIds, isActive) => executeBulkOperation(assortmentIds, async (assortmentId) => {
265
+ const assortment = await Assortments.findOne({ _id: assortmentId, deleted: null }, {});
266
+ if (!assortment)
267
+ throw new Error('not-found');
268
+ await updateAssortment(assortmentId, { isActive });
269
+ }),
270
+ bulkUpdateTags: async (assortmentIds, { add = [], remove = [] }) => {
271
+ const tagsToRemove = new Set(remove);
272
+ return executeBulkOperation(assortmentIds, async (assortmentId) => {
273
+ const assortment = await Assortments.findOne({ _id: assortmentId, deleted: null }, {});
274
+ if (!assortment)
275
+ throw new Error('not-found');
276
+ const tags = (assortment.tags || []).filter((tag) => !tagsToRemove.has(tag));
277
+ for (const tag of add) {
278
+ if (!tags.includes(tag))
279
+ tags.push(tag);
280
+ }
281
+ await updateAssortment(assortmentId, { tags });
282
+ });
250
283
  },
251
- invalidateCache,
252
284
  search: {
253
285
  findFilteredAssortments: async ({ limit, offset, assortmentIds, assortmentSelector, sort, }) => {
254
286
  const assortments = await findPreservingIds(Assortments)(assortmentSelector, assortmentIds, {
@@ -59,5 +59,18 @@ export default (tree) => {
59
59
  const levels = divideTreeByLevels(tree);
60
60
  const concattedLevels = concatItemsByLevels(levels);
61
61
  const items = shuffleEachLevel(concattedLevels);
62
- return items.flat(Infinity).filter(Boolean);
62
+ const result = [];
63
+ const stack = [...items].reverse();
64
+ while (stack.length > 0) {
65
+ const value = stack.pop();
66
+ if (Array.isArray(value)) {
67
+ for (let i = value.length - 1; i >= 0; i--) {
68
+ stack.push(value[i]);
69
+ }
70
+ }
71
+ else if (value) {
72
+ result.push(value);
73
+ }
74
+ }
75
+ return result;
63
76
  };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@unchainedshop/core-assortments",
3
3
  "description": "Assortment and category management module for the Unchained Engine",
4
- "version": "5.0.0-alpha.3",
4
+ "version": "5.0.0-alpha.5",
5
5
  "main": "lib/assortments-index.js",
6
6
  "types": "lib/assortments-index.d.ts",
7
7
  "type": "module",
@@ -35,13 +35,13 @@
35
35
  },
36
36
  "homepage": "https://github.com/unchainedshop/unchained#readme",
37
37
  "dependencies": {
38
- "@unchainedshop/events": "^5.0.0-alpha.1",
39
- "@unchainedshop/logger": "^5.0.0-alpha.1",
40
- "@unchainedshop/mongodb": "^5.0.0-alpha.1",
41
- "@unchainedshop/utils": "^5.0.0-alpha.1"
38
+ "@unchainedshop/events": "^5.0.0-alpha.5",
39
+ "@unchainedshop/logger": "^5.0.0-alpha.5",
40
+ "@unchainedshop/mongodb": "^5.0.0-alpha.5",
41
+ "@unchainedshop/utils": "^5.0.0-alpha.5"
42
42
  },
43
43
  "devDependencies": {
44
44
  "@types/node": "^26.2.0",
45
- "typescript": "^5.8.3"
45
+ "typescript": "^6.0.3"
46
46
  }
47
47
  }