@unchainedshop/core-assortments 5.0.0-alpha.1 → 5.0.0-alpha.10
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/README.md +12 -105
- package/lib/db/AssortmentMediaCollection.js +1 -1
- package/lib/db/AssortmentsCollection.js +23 -31
- package/lib/module/configureAssortmentFiltersModule.js +2 -2
- package/lib/module/configureAssortmentLinksModule.d.ts +3 -1
- package/lib/module/configureAssortmentLinksModule.js +18 -14
- package/lib/module/configureAssortmentMediaModule.js +2 -2
- package/lib/module/configureAssortmentProductsModule.d.ts +1 -1
- package/lib/module/configureAssortmentProductsModule.js +2 -2
- package/lib/module/configureAssortmentsModule.d.ts +19 -2
- package/lib/module/configureAssortmentsModule.js +74 -43
- package/lib/utils/tree-zipper/zipTreeByDeepness.js +34 -12
- package/package.json +10 -6
package/README.md
CHANGED
|
@@ -13,120 +13,27 @@ npm install @unchainedshop/core-assortments
|
|
|
13
13
|
|
|
14
14
|
## Usage
|
|
15
15
|
|
|
16
|
-
|
|
17
|
-
import { configureAssortmentsModule } from '@unchainedshop/core-assortments';
|
|
16
|
+
The platform initializes this module as `platform.unchainedAPI.modules.assortments`. For startup and options, see [Platform Configuration](https://docs.unchained.shop/platform-configuration/).
|
|
18
17
|
|
|
19
|
-
|
|
18
|
+
```typescript
|
|
19
|
+
const { assortments } = platform.unchainedAPI.modules;
|
|
20
20
|
|
|
21
|
-
|
|
22
|
-
const assortmentId = await assortmentsModule.create({
|
|
23
|
-
slugs: ['electronics'],
|
|
21
|
+
const assortment = await assortments.create({
|
|
24
22
|
isRoot: true,
|
|
23
|
+
isActive: true,
|
|
24
|
+
sequence: 10,
|
|
25
|
+
tags: [],
|
|
25
26
|
});
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
const assortments = await assortmentsModule.findAssortments({
|
|
29
|
-
includeInactive: false,
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
// Add product to assortment
|
|
33
|
-
await assortmentsModule.products.create({
|
|
34
|
-
assortmentId,
|
|
27
|
+
await assortments.products.create({
|
|
28
|
+
assortmentId: assortment._id,
|
|
35
29
|
productId: 'product-123',
|
|
36
30
|
});
|
|
31
|
+
const roots = await assortments.findAssortments({ includeLeaves: false, includeInactive: false });
|
|
37
32
|
```
|
|
38
33
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
### Module Configuration
|
|
42
|
-
|
|
43
|
-
| Export | Description |
|
|
44
|
-
|--------|-------------|
|
|
45
|
-
| `configureAssortmentsModule` | Configure and return the assortments module |
|
|
46
|
-
|
|
47
|
-
### Queries
|
|
48
|
-
|
|
49
|
-
| Method | Description |
|
|
50
|
-
|--------|-------------|
|
|
51
|
-
| `findAssortment` | Find assortment by ID or slug |
|
|
52
|
-
| `findAssortments` | Find assortments with filtering and pagination |
|
|
53
|
-
| `count` | Count assortments matching query |
|
|
54
|
-
| `assortmentExists` | Check if assortment exists |
|
|
55
|
-
| `findProductIds` | Get all product IDs in an assortment tree |
|
|
56
|
-
|
|
57
|
-
### Mutations
|
|
58
|
-
|
|
59
|
-
| Method | Description |
|
|
60
|
-
|--------|-------------|
|
|
61
|
-
| `create` | Create a new assortment |
|
|
62
|
-
| `update` | Update assortment data |
|
|
63
|
-
| `delete` | Soft delete an assortment |
|
|
64
|
-
| `setBase` | Set base assortment |
|
|
65
|
-
| `invalidateCache` | Clear assortment cache |
|
|
66
|
-
|
|
67
|
-
### Submodules
|
|
68
|
-
|
|
69
|
-
#### Products (`assortments.products`)
|
|
70
|
-
| Method | Description |
|
|
71
|
-
|--------|-------------|
|
|
72
|
-
| `findProducts` | Find products in assortment |
|
|
73
|
-
| `create` | Add product to assortment |
|
|
74
|
-
| `delete` | Remove product from assortment |
|
|
75
|
-
| `reorder` | Reorder products |
|
|
76
|
-
|
|
77
|
-
#### Links (`assortments.links`)
|
|
78
|
-
| Method | Description |
|
|
79
|
-
|--------|-------------|
|
|
80
|
-
| `findLinks` | Find assortment links (parent-child) |
|
|
81
|
-
| `create` | Create link between assortments |
|
|
82
|
-
| `delete` | Remove link |
|
|
83
|
-
| `reorder` | Reorder child assortments |
|
|
84
|
-
|
|
85
|
-
#### Media (`assortments.media`)
|
|
86
|
-
| Method | Description |
|
|
87
|
-
|--------|-------------|
|
|
88
|
-
| `findMedia` | Find assortment media |
|
|
89
|
-
| `create` | Add media to assortment |
|
|
90
|
-
| `delete` | Remove media |
|
|
91
|
-
| `reorder` | Reorder media |
|
|
92
|
-
|
|
93
|
-
#### Texts (`assortments.texts`)
|
|
94
|
-
| Method | Description |
|
|
95
|
-
|--------|-------------|
|
|
96
|
-
| `findTexts` | Find localized texts |
|
|
97
|
-
| `updateTexts` | Update assortment texts |
|
|
98
|
-
|
|
99
|
-
#### Filters (`assortments.filters`)
|
|
100
|
-
| Method | Description |
|
|
101
|
-
|--------|-------------|
|
|
102
|
-
| `findFilters` | Find filters for assortment |
|
|
103
|
-
| `create` | Add filter to assortment |
|
|
104
|
-
| `delete` | Remove filter |
|
|
105
|
-
| `reorder` | Reorder filters |
|
|
106
|
-
|
|
107
|
-
### Settings
|
|
108
|
-
|
|
109
|
-
| Export | Description |
|
|
110
|
-
|--------|-------------|
|
|
111
|
-
| `assortmentsSettings` | Access assortment module settings |
|
|
112
|
-
|
|
113
|
-
### Types
|
|
114
|
-
|
|
115
|
-
| Export | Description |
|
|
116
|
-
|--------|-------------|
|
|
117
|
-
| `Assortment` | Assortment document type |
|
|
118
|
-
| `AssortmentProduct` | Assortment-product link type |
|
|
119
|
-
| `AssortmentLink` | Parent-child link type |
|
|
120
|
-
| `AssortmentMedia` | Media attachment type |
|
|
121
|
-
| `AssortmentsModule` | Module interface type |
|
|
122
|
-
|
|
123
|
-
## Events
|
|
34
|
+
Product links, child links, media, texts, and filters live in the `products`, `links`, `media`, `texts`, and `filters` submodules. Creation returns the assortment document; use its `_id` when linking products.
|
|
124
35
|
|
|
125
|
-
|
|
126
|
-
|-------|-------------|
|
|
127
|
-
| `ASSORTMENT_CREATE` | Assortment created |
|
|
128
|
-
| `ASSORTMENT_UPDATE` | Assortment updated |
|
|
129
|
-
| `ASSORTMENT_REMOVE` | Assortment deleted |
|
|
36
|
+
See the [module guide](https://docs.unchained.shop/platform-configuration/modules/assortments) for caching, tree ordering, options, and events. The [public exports](src/assortments-index.ts) and [module implementation](src/module/configureAssortmentsModule.ts) define the available types and methods.
|
|
130
37
|
|
|
131
38
|
## License
|
|
132
39
|
|
|
@@ -10,7 +10,7 @@ export const AssortmentMediaCollection = async (db) => {
|
|
|
10
10
|
]);
|
|
11
11
|
await buildDbIndexes(AssortmentMediaTexts, [
|
|
12
12
|
{ index: { assortmentMediaId: 1 } },
|
|
13
|
-
{ index: { locale: 1 } },
|
|
13
|
+
{ index: { locale: 1, assortmentMediaId: 1 } },
|
|
14
14
|
]);
|
|
15
15
|
return {
|
|
16
16
|
AssortmentMedia,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { buildDbIndexes,
|
|
1
|
+
import { buildDbIndexes, mongodb } from '@unchainedshop/mongodb';
|
|
2
2
|
export const AssortmentsCollection = async (db) => {
|
|
3
3
|
const Assortments = db.collection('assortments');
|
|
4
4
|
const AssortmentTexts = db.collection('assortment_texts');
|
|
@@ -6,57 +6,49 @@ export const AssortmentsCollection = async (db) => {
|
|
|
6
6
|
const AssortmentLinks = db.collection('assortment_links');
|
|
7
7
|
const AssortmentFilters = db.collection('assortment_filters');
|
|
8
8
|
const AssortmentProductIdCache = db.collection('assortment_productId_cache');
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
{
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
name: 'assortments_fulltext_search',
|
|
15
|
-
},
|
|
9
|
+
await buildDbIndexes(Assortments, [
|
|
10
|
+
{
|
|
11
|
+
index: { slugs: 'text' },
|
|
12
|
+
options: {
|
|
13
|
+
name: 'assortments_fulltext_search',
|
|
16
14
|
},
|
|
17
|
-
|
|
18
|
-
|
|
15
|
+
},
|
|
16
|
+
]);
|
|
19
17
|
await buildDbIndexes(Assortments, [
|
|
20
|
-
{ index: { deleted: 1 } },
|
|
21
|
-
{ index: { isActive: 1 } },
|
|
22
|
-
{ index: { isRoot: 1 } },
|
|
23
|
-
{ index: { sequence: 1 } },
|
|
18
|
+
{ index: { deleted: 1, isActive: 1, isRoot: 1, sequence: 1 } },
|
|
24
19
|
{ index: { slugs: 1 } },
|
|
25
20
|
{ index: { tags: 1 } },
|
|
26
21
|
]);
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
{
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
subtitle: 6,
|
|
35
|
-
},
|
|
36
|
-
name: 'assortments_texts_fulltext_search',
|
|
22
|
+
await buildDbIndexes(AssortmentTexts, [
|
|
23
|
+
{
|
|
24
|
+
index: { title: 'text', subtitle: 'text' },
|
|
25
|
+
options: {
|
|
26
|
+
weights: {
|
|
27
|
+
title: 8,
|
|
28
|
+
subtitle: 6,
|
|
37
29
|
},
|
|
30
|
+
name: 'assortments_texts_fulltext_search',
|
|
38
31
|
},
|
|
39
|
-
|
|
40
|
-
|
|
32
|
+
},
|
|
33
|
+
]);
|
|
41
34
|
await buildDbIndexes(AssortmentTexts, [
|
|
42
35
|
{ index: { assortmentId: 1 } },
|
|
43
|
-
{ index: { locale: 1 } },
|
|
44
36
|
{ index: { slug: 1 } },
|
|
45
37
|
{ index: { locale: 1, assortmentId: 1 } },
|
|
46
38
|
]);
|
|
47
39
|
await buildDbIndexes(AssortmentProducts, [
|
|
48
|
-
{ index: { assortmentId: 1 } },
|
|
40
|
+
{ index: { assortmentId: 1, productId: 1 } },
|
|
49
41
|
{ index: { productId: 1 } },
|
|
50
42
|
{ index: { tags: 1 } },
|
|
51
43
|
{ index: { assortmentId: 1, sortKey: 1 } },
|
|
52
|
-
]);
|
|
44
|
+
], { rebuild: false });
|
|
53
45
|
await buildDbIndexes(AssortmentLinks, [
|
|
54
46
|
{ index: { parentAssortmentId: 1 } },
|
|
55
|
-
{ index: { childAssortmentId: 1 } },
|
|
47
|
+
{ index: { childAssortmentId: 1, sortKey: 1, parentAssortmentId: 1 } },
|
|
56
48
|
{ index: { tags: 1 } },
|
|
57
49
|
{ index: { parentAssortmentId: 1, sortKey: 1 } },
|
|
58
50
|
{ index: { childAssortmentId: 1, parentAssortmentId: 1, sortKey: 1 } },
|
|
59
|
-
]);
|
|
51
|
+
], { rebuild: false });
|
|
60
52
|
await buildDbIndexes(AssortmentFilters, [
|
|
61
53
|
{ index: { assortmentId: 1 } },
|
|
62
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
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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,6 +1,6 @@
|
|
|
1
|
-
import { SortDirection } from '@unchainedshop/utils';
|
|
1
|
+
import { executeBulkOperation, SortDirection } from '@unchainedshop/utils';
|
|
2
2
|
import { emit, registerEvents } from '@unchainedshop/events';
|
|
3
|
-
import { generateDbFilterById, findPreservingIds, buildSortOptions, mongodb, generateDbObjectId,
|
|
3
|
+
import { generateDbFilterById, findPreservingIds, buildSortOptions, mongodb, generateDbObjectId, } from '@unchainedshop/mongodb';
|
|
4
4
|
import { createLogger } from '@unchainedshop/logger';
|
|
5
5
|
import { AssortmentsCollection, } from "../db/AssortmentsCollection.js";
|
|
6
6
|
import { configureAssortmentFiltersModule } from "./configureAssortmentFiltersModule.js";
|
|
@@ -33,7 +33,6 @@ export const buildFindSelector = ({ assortmentIds, assortmentSelector, slugs, ta
|
|
|
33
33
|
selector.isRoot = true;
|
|
34
34
|
}
|
|
35
35
|
if (queryString) {
|
|
36
|
-
assertDocumentDBCompatMode();
|
|
37
36
|
selector.$text = { $search: queryString };
|
|
38
37
|
}
|
|
39
38
|
if (!assortmentSelector && !includeInactive) {
|
|
@@ -50,12 +49,12 @@ export const configureAssortmentsModule = async (moduleInput) => {
|
|
|
50
49
|
return AssortmentLinks.find({
|
|
51
50
|
$or: [{ parentAssortmentId: assortment._id }, { childAssortmentId: assortment._id }],
|
|
52
51
|
}, {
|
|
53
|
-
sort: { sortKey: 1 },
|
|
52
|
+
sort: { sortKey: 1, _id: 1 },
|
|
54
53
|
}).toArray();
|
|
55
54
|
};
|
|
56
55
|
const findProductAssignments = async (assortmentId) => {
|
|
57
56
|
return AssortmentProducts.find({ assortmentId }, {
|
|
58
|
-
sort: { sortKey: 1 },
|
|
57
|
+
sort: { sortKey: 1, _id: 1 },
|
|
59
58
|
}).toArray();
|
|
60
59
|
};
|
|
61
60
|
const collectProductIdCacheTree = async (assortment) => {
|
|
@@ -124,6 +123,42 @@ export const configureAssortmentsModule = async (moduleInput) => {
|
|
|
124
123
|
AssortmentTexts,
|
|
125
124
|
});
|
|
126
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
|
+
};
|
|
127
162
|
return {
|
|
128
163
|
findAssortment: async ({ assortmentId, slug }) => {
|
|
129
164
|
let selector = {};
|
|
@@ -167,7 +202,7 @@ export const configureAssortmentsModule = async (moduleInput) => {
|
|
|
167
202
|
children: async ({ assortmentId, includeInactive, }) => {
|
|
168
203
|
const links = await AssortmentLinks.find({ parentAssortmentId: assortmentId }, {
|
|
169
204
|
projection: { childAssortmentId: 1 },
|
|
170
|
-
sort: { sortKey: 1 },
|
|
205
|
+
sort: { sortKey: 1, _id: 1 },
|
|
171
206
|
}).toArray();
|
|
172
207
|
const assortmentIds = links.map(({ childAssortmentId }) => childAssortmentId);
|
|
173
208
|
const selector = !includeInactive ? { isActive: true } : {};
|
|
@@ -183,12 +218,12 @@ export const configureAssortmentsModule = async (moduleInput) => {
|
|
|
183
218
|
breadcrumbs: async (params, { resolveAssortmentLinks = async (id) => {
|
|
184
219
|
return AssortmentLinks.find({ childAssortmentId: id }, {
|
|
185
220
|
projection: { _id: 1, childAssortmentId: 1, parentAssortmentId: 1 },
|
|
186
|
-
sort: { sortKey: 1, parentAssortmentId: 1 },
|
|
221
|
+
sort: { sortKey: 1, parentAssortmentId: 1, _id: 1 },
|
|
187
222
|
}).toArray();
|
|
188
223
|
}, resolveAssortmentProducts = async (id) => {
|
|
189
224
|
return AssortmentProducts.find({ productId: id }, {
|
|
190
225
|
projection: { _id: true, assortmentId: true, productId: true },
|
|
191
|
-
sort: { sortKey: 1, productId: 1 },
|
|
226
|
+
sort: { sortKey: 1, productId: 1, _id: 1 },
|
|
192
227
|
}).toArray();
|
|
193
228
|
}, }) => {
|
|
194
229
|
const buildBreadcrumbs = makeAssortmentBreadcrumbsBuilder({
|
|
@@ -213,43 +248,39 @@ export const configureAssortmentsModule = async (moduleInput) => {
|
|
|
213
248
|
await emit('ASSORTMENT_CREATE', { assortment });
|
|
214
249
|
return assortment;
|
|
215
250
|
},
|
|
216
|
-
update:
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
},
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
}
|
|
229
|
-
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
|
+
});
|
|
230
263
|
},
|
|
231
|
-
|
|
232
|
-
await
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
await
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
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
|
+
});
|
|
251
283
|
},
|
|
252
|
-
invalidateCache,
|
|
253
284
|
search: {
|
|
254
285
|
findFilteredAssortments: async ({ limit, offset, assortmentIds, assortmentSelector, sort, }) => {
|
|
255
286
|
const assortments = await findPreservingIds(Assortments)(assortmentSelector, assortmentIds, {
|
|
@@ -5,19 +5,28 @@ export const fillToSameLengthArray = (a, b) => {
|
|
|
5
5
|
return [fillUp(a, length), fillUp(b, length)];
|
|
6
6
|
};
|
|
7
7
|
export const divideTreeByLevels = (array, level = 0) => {
|
|
8
|
-
const
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
const result = [];
|
|
9
|
+
const stack = [{ array, level }];
|
|
10
|
+
while (stack.length > 0) {
|
|
11
|
+
const { array: current, level: currentLevel } = stack.pop();
|
|
12
|
+
const items = [];
|
|
13
|
+
const children = [];
|
|
14
|
+
for (const item of current) {
|
|
15
|
+
if (typeof item === 'object') {
|
|
16
|
+
children.push(item);
|
|
17
|
+
}
|
|
18
|
+
else {
|
|
19
|
+
items.push(item);
|
|
20
|
+
}
|
|
11
21
|
}
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
const nextLevels = array.reduce((acc, item) => {
|
|
15
|
-
if (typeof item === 'object') {
|
|
16
|
-
return [...acc, ...divideTreeByLevels(item, level + 1)];
|
|
22
|
+
if (items.length) {
|
|
23
|
+
result.push({ level: currentLevel, items });
|
|
17
24
|
}
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
25
|
+
for (let i = children.length - 1; i >= 0; i--) {
|
|
26
|
+
stack.push({ array: children[i], level: currentLevel + 1 });
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return result;
|
|
21
30
|
};
|
|
22
31
|
export const concatItemsByLevels = (levelArray) => {
|
|
23
32
|
return Object.values(levelArray.reduce((acc, { level, items }) => {
|
|
@@ -50,5 +59,18 @@ export default (tree) => {
|
|
|
50
59
|
const levels = divideTreeByLevels(tree);
|
|
51
60
|
const concattedLevels = concatItemsByLevels(levels);
|
|
52
61
|
const items = shuffleEachLevel(concattedLevels);
|
|
53
|
-
|
|
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;
|
|
54
76
|
};
|
package/package.json
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
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.
|
|
4
|
+
"version": "5.0.0-alpha.10",
|
|
5
|
+
"files": [
|
|
6
|
+
"lib"
|
|
7
|
+
],
|
|
5
8
|
"main": "lib/assortments-index.js",
|
|
6
9
|
"types": "lib/assortments-index.d.ts",
|
|
7
10
|
"type": "module",
|
|
@@ -35,12 +38,13 @@
|
|
|
35
38
|
},
|
|
36
39
|
"homepage": "https://github.com/unchainedshop/unchained#readme",
|
|
37
40
|
"dependencies": {
|
|
38
|
-
"@unchainedshop/events": "^5.0.0-alpha.
|
|
39
|
-
"@unchainedshop/logger": "^5.0.0-alpha.
|
|
40
|
-
"@unchainedshop/
|
|
41
|
+
"@unchainedshop/events": "^5.0.0-alpha.10",
|
|
42
|
+
"@unchainedshop/logger": "^5.0.0-alpha.10",
|
|
43
|
+
"@unchainedshop/mongodb": "^5.0.0-alpha.10",
|
|
44
|
+
"@unchainedshop/utils": "^5.0.0-alpha.10"
|
|
41
45
|
},
|
|
42
46
|
"devDependencies": {
|
|
43
|
-
"@types/node": "^
|
|
44
|
-
"typescript": "^
|
|
47
|
+
"@types/node": "^26.2.0",
|
|
48
|
+
"typescript": "^6.0.3"
|
|
45
49
|
}
|
|
46
50
|
}
|