@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,234 @@
|
|
|
1
|
+
import { AssortmentProduct, AssortmentsModule } from '@unchainedshop/types/assortments';
|
|
2
|
+
import { Collection } from '@unchainedshop/types/common';
|
|
3
|
+
import { emit, registerEvents } from '@unchainedshop/events';
|
|
4
|
+
import { generateDbFilterById, generateDbObjectId } from '@unchainedshop/utils';
|
|
5
|
+
|
|
6
|
+
const ASSORTMENT_PRODUCT_EVENTS = [
|
|
7
|
+
'ASSORTMENT_ADD_PRODUCT',
|
|
8
|
+
'ASSORTMENT_REMOVE_PRODUCT',
|
|
9
|
+
'ASSORTMENT_REORDER_PRODUCTS',
|
|
10
|
+
];
|
|
11
|
+
|
|
12
|
+
export const configureAssortmentProductsModule = ({
|
|
13
|
+
AssortmentProducts,
|
|
14
|
+
invalidateCache,
|
|
15
|
+
}: {
|
|
16
|
+
AssortmentProducts: Collection<AssortmentProduct>;
|
|
17
|
+
invalidateCache: AssortmentsModule['invalidateCache'];
|
|
18
|
+
}): AssortmentsModule['products'] => {
|
|
19
|
+
registerEvents(ASSORTMENT_PRODUCT_EVENTS);
|
|
20
|
+
|
|
21
|
+
return {
|
|
22
|
+
// Queries
|
|
23
|
+
findAssortmentIds: async ({ productId, tags }) => {
|
|
24
|
+
const selector = { productId };
|
|
25
|
+
if (tags) {
|
|
26
|
+
selector.tags = { $in: tags };
|
|
27
|
+
}
|
|
28
|
+
return AssortmentProducts.find(selector, { projection: { assortmentId: true } })
|
|
29
|
+
.map(({ assortmentId }) => assortmentId)
|
|
30
|
+
.toArray();
|
|
31
|
+
},
|
|
32
|
+
|
|
33
|
+
findProductIds: async ({ assortmentId, tags }) => {
|
|
34
|
+
const selector = { assortmentId };
|
|
35
|
+
if (tags) {
|
|
36
|
+
selector.tags = { $in: tags };
|
|
37
|
+
}
|
|
38
|
+
return AssortmentProducts.find(selector, { projection: { productId: true } })
|
|
39
|
+
.map(({ productId }) => productId)
|
|
40
|
+
.toArray();
|
|
41
|
+
},
|
|
42
|
+
|
|
43
|
+
findProduct: async ({ assortmentProductId }) => {
|
|
44
|
+
return AssortmentProducts.findOne(generateDbFilterById(assortmentProductId), {});
|
|
45
|
+
},
|
|
46
|
+
|
|
47
|
+
findProducts: async ({ assortmentId }, options) => {
|
|
48
|
+
const products = AssortmentProducts.find({ assortmentId }, options);
|
|
49
|
+
return products.toArray();
|
|
50
|
+
},
|
|
51
|
+
|
|
52
|
+
findProductSiblings: async ({ assortmentIds, productId }) => {
|
|
53
|
+
const selector = {
|
|
54
|
+
$and: [
|
|
55
|
+
{
|
|
56
|
+
productId: { $ne: productId },
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
assortmentId: { $in: assortmentIds },
|
|
60
|
+
},
|
|
61
|
+
],
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
return AssortmentProducts.find(selector, {
|
|
65
|
+
projection: { productId: 1 },
|
|
66
|
+
})
|
|
67
|
+
.map((product) => product.productId)
|
|
68
|
+
.toArray();
|
|
69
|
+
},
|
|
70
|
+
|
|
71
|
+
// Mutations
|
|
72
|
+
create: async (doc: AssortmentProduct, options, userId) => {
|
|
73
|
+
const { _id, assortmentId, productId, sortKey, ...rest } = doc;
|
|
74
|
+
|
|
75
|
+
const selector = {
|
|
76
|
+
...(doc._id ? generateDbFilterById(doc._id) : {}),
|
|
77
|
+
productId,
|
|
78
|
+
assortmentId,
|
|
79
|
+
};
|
|
80
|
+
const $set: any = {
|
|
81
|
+
updated: new Date(),
|
|
82
|
+
updatedBy: userId,
|
|
83
|
+
...rest,
|
|
84
|
+
};
|
|
85
|
+
const $setOnInsert: any = {
|
|
86
|
+
_id: _id || generateDbObjectId(),
|
|
87
|
+
productId,
|
|
88
|
+
assortmentId,
|
|
89
|
+
created: new Date(),
|
|
90
|
+
createdBy: userId,
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
if (!sortKey) {
|
|
94
|
+
// Get next sort key
|
|
95
|
+
const lastAssortmentProduct = (await AssortmentProducts.findOne(
|
|
96
|
+
{ assortmentId },
|
|
97
|
+
{ sort: { sortKey: -1 } },
|
|
98
|
+
)) || { sortKey: 0 };
|
|
99
|
+
$setOnInsert.sortKey = lastAssortmentProduct.sortKey + 1;
|
|
100
|
+
} else {
|
|
101
|
+
$set.sortKey = sortKey;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
await AssortmentProducts.updateOne(
|
|
105
|
+
selector,
|
|
106
|
+
{
|
|
107
|
+
$set,
|
|
108
|
+
$setOnInsert,
|
|
109
|
+
},
|
|
110
|
+
{ upsert: true },
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
const assortmentProduct = await AssortmentProducts.findOne(selector, {});
|
|
114
|
+
|
|
115
|
+
emit('ASSORTMENT_ADD_PRODUCT', { assortmentProduct });
|
|
116
|
+
|
|
117
|
+
if (!options.skipInvalidation) {
|
|
118
|
+
await invalidateCache(
|
|
119
|
+
{ assortmentIds: [assortmentProduct.assortmentId] },
|
|
120
|
+
{ skipUpstreamTraversal: false },
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return assortmentProduct;
|
|
125
|
+
},
|
|
126
|
+
|
|
127
|
+
delete: async (assortmentProductId, options) => {
|
|
128
|
+
const selector = generateDbFilterById(assortmentProductId);
|
|
129
|
+
|
|
130
|
+
const assortmentProduct = await AssortmentProducts.findOne(selector, {
|
|
131
|
+
projection: { _id: 1, assortmentId: 1 },
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
if (!assortmentProduct) return [];
|
|
135
|
+
|
|
136
|
+
await AssortmentProducts.deleteOne(selector);
|
|
137
|
+
|
|
138
|
+
emit('ASSORTMENT_REMOVE_PRODUCT', {
|
|
139
|
+
assortmentProductId: assortmentProduct._id,
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
if (!options.skipInvalidation) {
|
|
143
|
+
await invalidateCache(
|
|
144
|
+
{ assortmentIds: [assortmentProduct.assortmentId] },
|
|
145
|
+
{
|
|
146
|
+
skipUpstreamTraversal: false,
|
|
147
|
+
},
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return [assortmentProduct];
|
|
152
|
+
},
|
|
153
|
+
|
|
154
|
+
deleteMany: async (selector, options) => {
|
|
155
|
+
const assortmentProducts = await AssortmentProducts.find(selector, {
|
|
156
|
+
projection: { _id: 1, assortmentId: 1 },
|
|
157
|
+
}).toArray();
|
|
158
|
+
|
|
159
|
+
await AssortmentProducts.deleteMany(selector);
|
|
160
|
+
assortmentProducts.forEach((assortmentProduct) => {
|
|
161
|
+
emit('ASSORTMENT_REMOVE_PRODUCT', {
|
|
162
|
+
assortmentProductId: assortmentProduct._id,
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
if (!options.skipInvalidation && assortmentProducts.length) {
|
|
167
|
+
await invalidateCache(
|
|
168
|
+
{ assortmentIds: assortmentProducts.map((product) => product.assortmentId) },
|
|
169
|
+
{
|
|
170
|
+
skipUpstreamTraversal: false,
|
|
171
|
+
},
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return assortmentProducts;
|
|
176
|
+
},
|
|
177
|
+
|
|
178
|
+
// This action is specifically used for the bulk migration scripts in the platform package
|
|
179
|
+
update: async (assortmentProductId, doc, options, userId) => {
|
|
180
|
+
const selector = generateDbFilterById(assortmentProductId);
|
|
181
|
+
const modifier = {
|
|
182
|
+
$set: {
|
|
183
|
+
...doc,
|
|
184
|
+
updated: new Date(),
|
|
185
|
+
updatedBy: userId,
|
|
186
|
+
},
|
|
187
|
+
};
|
|
188
|
+
await AssortmentProducts.updateOne(selector, modifier);
|
|
189
|
+
|
|
190
|
+
const assortmentProduct = await AssortmentProducts.findOne(selector, {});
|
|
191
|
+
if (!options.skipInvalidation) {
|
|
192
|
+
await invalidateCache(
|
|
193
|
+
{ assortmentIds: [assortmentProduct.assortmentId] },
|
|
194
|
+
{
|
|
195
|
+
skipUpstreamTraversal: false,
|
|
196
|
+
},
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
return assortmentProduct;
|
|
200
|
+
},
|
|
201
|
+
|
|
202
|
+
updateManualOrder: async ({ sortKeys }, options, userId) => {
|
|
203
|
+
const changedAssortmentProductIds = await Promise.all(
|
|
204
|
+
sortKeys.map(async ({ assortmentProductId, sortKey }) => {
|
|
205
|
+
await AssortmentProducts.updateOne(generateDbFilterById(assortmentProductId), {
|
|
206
|
+
$set: {
|
|
207
|
+
sortKey: sortKey + 1,
|
|
208
|
+
updated: new Date(),
|
|
209
|
+
updatedBy: userId,
|
|
210
|
+
},
|
|
211
|
+
});
|
|
212
|
+
return assortmentProductId;
|
|
213
|
+
}),
|
|
214
|
+
);
|
|
215
|
+
|
|
216
|
+
const assortmentProducts = await AssortmentProducts.find({
|
|
217
|
+
_id: { $in: changedAssortmentProductIds },
|
|
218
|
+
}).toArray();
|
|
219
|
+
|
|
220
|
+
if (!options.skipInvalidation && assortmentProducts.length) {
|
|
221
|
+
await invalidateCache(
|
|
222
|
+
{ assortmentIds: assortmentProducts.map((product) => product.assortmentId) },
|
|
223
|
+
{
|
|
224
|
+
skipUpstreamTraversal: false,
|
|
225
|
+
},
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
emit('ASSORTMENT_REORDER_PRODUCTS', { assortmentProducts });
|
|
230
|
+
|
|
231
|
+
return assortmentProducts;
|
|
232
|
+
},
|
|
233
|
+
};
|
|
234
|
+
};
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import { Assortment, AssortmentsModule, AssortmentText } from '@unchainedshop/types/assortments';
|
|
2
|
+
import { Collection } from '@unchainedshop/types/common';
|
|
3
|
+
import { Locale } from 'locale';
|
|
4
|
+
import { emit, registerEvents } from '@unchainedshop/events';
|
|
5
|
+
import {
|
|
6
|
+
findLocalizedText,
|
|
7
|
+
findUnusedSlug,
|
|
8
|
+
generateDbFilterById,
|
|
9
|
+
generateDbObjectId,
|
|
10
|
+
} from '@unchainedshop/utils';
|
|
11
|
+
import { assortmentsSettings } from '../assortments-settings';
|
|
12
|
+
|
|
13
|
+
const ASSORTMENT_TEXT_EVENTS = ['ASSORTMENT_UPDATE_TEXTS'];
|
|
14
|
+
|
|
15
|
+
export const configureAssortmentTextsModule = ({
|
|
16
|
+
Assortments,
|
|
17
|
+
AssortmentTexts,
|
|
18
|
+
}: {
|
|
19
|
+
Assortments: Collection<Assortment>;
|
|
20
|
+
AssortmentTexts: Collection<AssortmentText>;
|
|
21
|
+
}): AssortmentsModule['texts'] => {
|
|
22
|
+
registerEvents(ASSORTMENT_TEXT_EVENTS);
|
|
23
|
+
|
|
24
|
+
const makeSlug = async ({ slug, title, assortmentId }) => {
|
|
25
|
+
const checkSlugIsUnique = async (newPotentialSlug: string) => {
|
|
26
|
+
return (
|
|
27
|
+
(await AssortmentTexts.countDocuments(
|
|
28
|
+
{
|
|
29
|
+
assortmentId: { $ne: assortmentId },
|
|
30
|
+
slug: newPotentialSlug,
|
|
31
|
+
},
|
|
32
|
+
{ limit: 1 },
|
|
33
|
+
)) === 0
|
|
34
|
+
);
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const findSlug = findUnusedSlug(checkSlugIsUnique, { slugify: assortmentsSettings.slugify });
|
|
38
|
+
return findSlug({
|
|
39
|
+
existingSlug: slug,
|
|
40
|
+
title: title || assortmentId,
|
|
41
|
+
});
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const upsertLocalizedText: AssortmentsModule['texts']['upsertLocalizedText'] = async (
|
|
45
|
+
assortmentId,
|
|
46
|
+
locale,
|
|
47
|
+
text,
|
|
48
|
+
userId,
|
|
49
|
+
) => {
|
|
50
|
+
const { slug: textSlug, ...textFields } = text;
|
|
51
|
+
const slug = await makeSlug({
|
|
52
|
+
slug: textSlug,
|
|
53
|
+
title: text.title,
|
|
54
|
+
assortmentId,
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
const modifier: any = {
|
|
58
|
+
$set: {
|
|
59
|
+
updated: new Date(),
|
|
60
|
+
updatedBy: userId,
|
|
61
|
+
authorId: userId,
|
|
62
|
+
...textFields,
|
|
63
|
+
},
|
|
64
|
+
$setOnInsert: {
|
|
65
|
+
_id: generateDbObjectId(),
|
|
66
|
+
created: new Date(),
|
|
67
|
+
createdBy: userId,
|
|
68
|
+
locale,
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
if (text.slug) {
|
|
73
|
+
modifier.$set.slug = slug;
|
|
74
|
+
} else {
|
|
75
|
+
modifier.$setOnInsert.slug = slug;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const selector = { assortmentId, locale };
|
|
79
|
+
|
|
80
|
+
const updateResult = await AssortmentTexts.updateOne(selector, modifier, {
|
|
81
|
+
upsert: true,
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
if (updateResult.upsertedCount > 0 || updateResult.modifiedCount > 0) {
|
|
85
|
+
const assortmentSelector = generateDbFilterById(assortmentId);
|
|
86
|
+
await Assortments.updateOne(assortmentSelector, {
|
|
87
|
+
$set: {
|
|
88
|
+
updated: new Date(),
|
|
89
|
+
updatedBy: userId,
|
|
90
|
+
},
|
|
91
|
+
$addToSet: {
|
|
92
|
+
slugs: slug,
|
|
93
|
+
},
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
await Assortments.updateMany(
|
|
97
|
+
{
|
|
98
|
+
_id: { $ne: assortmentId },
|
|
99
|
+
slugs: slug,
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
$set: {
|
|
103
|
+
updated: new Date(),
|
|
104
|
+
updatedBy: userId,
|
|
105
|
+
},
|
|
106
|
+
$pull: {
|
|
107
|
+
slugs: slug,
|
|
108
|
+
},
|
|
109
|
+
},
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return AssortmentTexts.findOne(
|
|
114
|
+
updateResult.upsertedId ? generateDbFilterById(updateResult.upsertedId) : selector,
|
|
115
|
+
{},
|
|
116
|
+
);
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
return {
|
|
120
|
+
// Queries
|
|
121
|
+
findTexts: async (query, options) => {
|
|
122
|
+
const texts = AssortmentTexts.find(query, options);
|
|
123
|
+
|
|
124
|
+
return texts.toArray();
|
|
125
|
+
},
|
|
126
|
+
|
|
127
|
+
findLocalizedText: async ({ assortmentId, locale }) => {
|
|
128
|
+
const parsedLocale = new Locale(locale);
|
|
129
|
+
|
|
130
|
+
const text = await findLocalizedText<AssortmentText>(
|
|
131
|
+
AssortmentTexts,
|
|
132
|
+
{ assortmentId },
|
|
133
|
+
parsedLocale,
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
return text;
|
|
137
|
+
},
|
|
138
|
+
|
|
139
|
+
searchTexts: async ({ searchText }) => {
|
|
140
|
+
const assortmentIds = AssortmentTexts.find(
|
|
141
|
+
{ $text: { $search: searchText } },
|
|
142
|
+
{
|
|
143
|
+
projection: {
|
|
144
|
+
assortmentId: 1,
|
|
145
|
+
},
|
|
146
|
+
},
|
|
147
|
+
).map(({ assortmentId }) => assortmentId);
|
|
148
|
+
|
|
149
|
+
return assortmentIds.toArray();
|
|
150
|
+
},
|
|
151
|
+
|
|
152
|
+
// Mutations
|
|
153
|
+
updateTexts: async (assortmentId, texts, userId) => {
|
|
154
|
+
const assortmentTexts = Array.isArray(texts)
|
|
155
|
+
? await Promise.all(
|
|
156
|
+
texts.map(async ({ locale, ...text }) =>
|
|
157
|
+
upsertLocalizedText(assortmentId, locale, text, userId),
|
|
158
|
+
),
|
|
159
|
+
)
|
|
160
|
+
: [];
|
|
161
|
+
|
|
162
|
+
emit('ASSORTMENT_UPDATE_TEXTS', {
|
|
163
|
+
assortmentId,
|
|
164
|
+
assortmentTexts,
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
return assortmentTexts;
|
|
168
|
+
},
|
|
169
|
+
|
|
170
|
+
upsertLocalizedText,
|
|
171
|
+
makeSlug,
|
|
172
|
+
|
|
173
|
+
deleteMany: async ({ assortmentId }) => {
|
|
174
|
+
const deletedResult = await AssortmentTexts.deleteMany({ assortmentId });
|
|
175
|
+
|
|
176
|
+
return deletedResult.deletedCount;
|
|
177
|
+
},
|
|
178
|
+
};
|
|
179
|
+
};
|