@storecraft/database-mongodb 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,537 @@
1
+ import { Collection } from 'mongodb'
2
+ import { MongoDB } from '../index.js'
3
+ import {
4
+ count_regular, expand, get_bulk, get_regular, list_regular,
5
+ zeroed_relations
6
+ } from './con.shared.js'
7
+ import {
8
+ delete_keys, handle_or_id, sanitize_array, sanitize_one, to_objid
9
+ } from './utils.funcs.js'
10
+ import {
11
+ add_search_terms_relation_on, create_explicit_relation,
12
+ delete_me,
13
+ remove_entry_from_all_connection_of_relation,
14
+ remove_specific_connection_of_relation,
15
+ remove_specific_connection_of_relation_with_filter,
16
+ save_me,
17
+ update_entry_on_all_connection_of_relation,
18
+ update_specific_connection_of_relation,
19
+ update_specific_connection_of_relation_with_filter
20
+ } from './utils.relations.js'
21
+ import { enums } from '@storecraft/core/v-api'
22
+ import { report_document_media } from './con.images.js'
23
+ import { union } from '@storecraft/core/v-api/utils.func.js'
24
+ import {
25
+ test_product_filters_against_product
26
+ } from '@storecraft/core/v-api/con.pricing.logic.js'
27
+
28
+ /**
29
+ * @typedef {import('@storecraft/core/v-database').db_products} db_col
30
+ */
31
+
32
+ /**
33
+ * @param {MongoDB} d
34
+ *
35
+ *
36
+ * @returns {Collection<
37
+ * import('./utils.relations.js').WithRelations<db_col["$type_get"]>
38
+ * >}
39
+ *
40
+ */
41
+ const col = (d) => d.collection('products');
42
+
43
+ /**
44
+ * @param {MongoDB} driver
45
+ *
46
+ *
47
+ * @returns {db_col["upsert"]}
48
+ */
49
+ const upsert = (driver) => {
50
+ return async (data, search_terms=[]) => {
51
+ // console.log('search_terms', search_terms)
52
+ data = {...data};
53
+
54
+ const objid = to_objid(data.id);
55
+ const session = driver.mongo_client.startSession();
56
+
57
+ try {
58
+ await session.withTransaction(
59
+ async () => {
60
+
61
+ ////
62
+ // COLLECTIONS RELATION (explicit)
63
+ ////
64
+ let replacement = await create_explicit_relation(
65
+ driver, data, 'collections', 'collections', true
66
+ );
67
+
68
+ ////
69
+ // PRODUCTS -> Related Products RELATION (explicit)
70
+ ////
71
+ replacement = await create_explicit_relation(
72
+ driver, replacement, 'related_products', 'products', true
73
+ );
74
+
75
+ ////
76
+ // DISCOUNTS RELATION
77
+ ////
78
+ // get all automatic + active discounts
79
+ const discounts = await driver.resources.discounts._col.find(
80
+ {
81
+ 'application.id': enums.DiscountApplicationEnum.Auto.id,
82
+ active: true
83
+ }, {
84
+ limit: 1000,
85
+ projection: zeroed_relations
86
+ }
87
+ ).toArray();
88
+
89
+ // now test locally
90
+ const eligible_discounts = discounts.filter(
91
+ d => test_product_filters_against_product(
92
+ d.info.filters, data
93
+ )
94
+ );
95
+
96
+ // console.log('eligible_discounts', eligible_discounts)
97
+
98
+ // now replace discounts relation
99
+ replacement._relations = replacement._relations ?? {};
100
+ replacement._relations.discounts = {
101
+ ids: eligible_discounts.map(d => d._id),
102
+ entries: Object.fromEntries(
103
+ eligible_discounts.map(d => [d._id.toString(), d])
104
+ )
105
+ }
106
+
107
+ // SEARCH
108
+ add_search_terms_relation_on(
109
+ replacement, union(
110
+ [
111
+ search_terms,
112
+ eligible_discounts.map(d => `discount:${d.handle}`),
113
+ eligible_discounts.map(d => `discount:${d.id}`),
114
+ ]
115
+ )
116
+ );
117
+
118
+ delete_keys(
119
+ 'collections', 'variants', 'discounts', 'related_products', 'search'
120
+ )(replacement);
121
+
122
+ // Now update other relations, that point to me
123
+
124
+ ////
125
+ // Related Products -> PRODUCTS RELATION (explicit)
126
+ ////
127
+ await update_entry_on_all_connection_of_relation(
128
+ driver, 'products', 'related_products', objid, replacement, session
129
+ );
130
+
131
+ ////
132
+ // VARIANTS RELATION
133
+ ////
134
+
135
+ if(`parent_handle` in data) { // is variant ?
136
+ // TODO: stronger validation of variant identification
137
+
138
+ const isValid = (
139
+ data.parent_handle && data.parent_id && data.variant_hint
140
+ );
141
+
142
+ if(isValid) {
143
+ // update parent product
144
+ await update_specific_connection_of_relation(
145
+ driver, 'products', 'variants', to_objid(data.parent_id),
146
+ objid, replacement, session
147
+ );
148
+
149
+ }
150
+
151
+ } else { // i am a parent
152
+ // let's fetch existing relations if any
153
+ const existing = await await col(driver).findOne(
154
+ { _id: objid }
155
+ );
156
+ if(existing && existing?._relations?.variants) {
157
+ replacement._relations = replacement._relations ?? {};
158
+ replacement._relations.variants = existing._relations.variants;
159
+ }
160
+ }
161
+
162
+ ////
163
+ // STOREFRONTS -> PRODUCTS RELATION
164
+ ////
165
+ await update_entry_on_all_connection_of_relation(
166
+ driver, 'storefronts', 'products', objid, replacement, session
167
+ );
168
+
169
+ ////
170
+ // REPORT IMAGES USAGE
171
+ ////
172
+ await report_document_media(driver)(replacement, session);
173
+
174
+ // SAVE ME
175
+ await save_me(driver, 'products', objid, replacement, session);
176
+
177
+ }
178
+ );
179
+ } catch (e) {
180
+ console.log(e);
181
+
182
+ return false;
183
+ } finally {
184
+ await session.endSession();
185
+ }
186
+
187
+ return true;
188
+ }
189
+ }
190
+
191
+
192
+ /**
193
+ *
194
+ * @param {MongoDB} driver
195
+ */
196
+ const get = (driver) => get_regular(driver, col(driver));
197
+
198
+ /**
199
+ * @param {MongoDB} driver
200
+ *
201
+ *
202
+ * @returns {db_col["remove"]}
203
+ */
204
+ const remove = (driver) => {
205
+ return async (id) => {
206
+ // todo: transaction
207
+
208
+ const item = await col(driver).findOne(handle_or_id(id));
209
+
210
+ if(!item) return;
211
+
212
+ const objid = to_objid(item.id);
213
+ const session = driver.mongo_client.startSession();
214
+
215
+ try {
216
+ await session.withTransaction(
217
+ async () => {
218
+
219
+ ////
220
+ // PRODUCTS -> VARIANTS RELATION
221
+ ////
222
+ const is_variant = `parent_handle` in item;
223
+
224
+ if(is_variant) {
225
+ // TODO: stronger validation
226
+ const isValid = (
227
+ item.parent_handle && item.parent_id && item.variant_hint
228
+ );
229
+
230
+ if(isValid) {
231
+ // remove me from parent
232
+ await remove_specific_connection_of_relation(
233
+ driver, 'products', 'variants', to_objid(item.parent_id),
234
+ objid, session
235
+ );
236
+
237
+ }
238
+
239
+ } else {
240
+ // I am a parent, let's delete all of the children variants
241
+ const ids = item?._relations?.variants?.ids;
242
+ if(ids) {
243
+ await driver.resources.products._col.deleteMany(
244
+ { _id: { $in: ids } },
245
+ { session }
246
+ );
247
+ }
248
+ }
249
+
250
+ ////
251
+ // STOREFRONTS --> PRODUCTS RELATION
252
+ ////
253
+ await remove_entry_from_all_connection_of_relation(
254
+ driver, 'storefronts', 'products', objid, session
255
+ );
256
+
257
+ ////
258
+ // PRODUCTS --> RELATED PRODUCTS RELATION
259
+ ////
260
+ await remove_entry_from_all_connection_of_relation(
261
+ driver, 'products', 'related_products', objid, session
262
+ );
263
+
264
+ // DELETE ME
265
+ await delete_me(
266
+ driver, 'products', objid, session
267
+ );
268
+
269
+ }
270
+ );
271
+ } catch (e) {
272
+ console.log(e);
273
+ return false;
274
+ } finally {
275
+ await session.endSession();
276
+ }
277
+
278
+ return true;
279
+ }
280
+
281
+ }
282
+
283
+ /**
284
+ * @param {MongoDB} driver
285
+ */
286
+ const list = (driver) => list_regular(driver, col(driver));
287
+
288
+ /**
289
+ * @param {MongoDB} driver
290
+ */
291
+ const count = (driver) => count_regular(driver, col(driver));
292
+
293
+
294
+ /**
295
+ * For now and because each product is related to very few
296
+ * collections, I will not expose the query api, and use aggregate
297
+ * instead.
298
+ *
299
+ *
300
+ * @param {MongoDB} driver
301
+ *
302
+ *
303
+ * @returns {db_col["list_product_collections"]}
304
+ */
305
+ const list_product_collections = (driver) => {
306
+ return async (product) => {
307
+ /** @type {import('@storecraft/core/v-database').RegularGetOptions} */
308
+ const options = {
309
+ expand: ['collections']
310
+ };
311
+
312
+ // We have collections embedded in products, so let's use it
313
+ const item = await get_regular(driver, col(driver))(product, options);
314
+
315
+ return sanitize_array(item?.collections ?? []);
316
+ }
317
+ }
318
+
319
+ /**
320
+ * For now and because each product is related to very few
321
+ * collections, I will not expose the query api, and use aggregate
322
+ * instead.
323
+ *
324
+ *
325
+ * @param {MongoDB} driver
326
+ *
327
+ *
328
+ * @returns {db_col["list_product_variants"]}
329
+ */
330
+ const list_product_variants = (driver) => {
331
+ return async (product) => {
332
+ /** @type {import('@storecraft/core/v-database').RegularGetOptions} */
333
+ const options = {
334
+ expand: ['variants']
335
+ };
336
+
337
+ // We have collections embedded in products, so let's use it
338
+ const item = await get_regular(driver, col(driver))(product, options);
339
+
340
+ if(item && ('variants' in item)) {
341
+
342
+ return sanitize_array(item?.variants ?? []);
343
+ }
344
+
345
+ return [];
346
+ }
347
+ }
348
+
349
+ /**
350
+ * For now and because each product is related to very few
351
+ * collections, I will not expose the query api, and use aggregate
352
+ * instead.
353
+ *
354
+ *
355
+ * @param {MongoDB} driver
356
+ *
357
+ *
358
+ * @returns {db_col["list_related_products"]}
359
+ */
360
+ const list_related_products = (driver) => {
361
+ return async (product) => {
362
+ /** @type {import('@storecraft/core/v-database').RegularGetOptions} */
363
+ const options = {
364
+ expand: ['related_products']
365
+ };
366
+
367
+ // We have collections embedded in products, so let's use it
368
+ const item = await get_regular(driver, col(driver))(product, options);
369
+
370
+ return sanitize_array(item?.related_products ?? []);
371
+ }
372
+ }
373
+
374
+
375
+ /**
376
+ * @param {MongoDB} driver
377
+ *
378
+ *
379
+ * @returns {db_col["list_product_discounts"]}
380
+ */
381
+ const list_product_discounts = (driver) => {
382
+ return async (product) => {
383
+ /** @type {import('@storecraft/core/v-database').RegularGetOptions} */
384
+ const options = {
385
+ expand: ['discounts']
386
+ };
387
+
388
+ // We have collections embedded in products, so let's use it
389
+ const item = await get_regular(driver, col(driver))(product, options);
390
+
391
+ return sanitize_array(item?.discounts ?? []);
392
+ }
393
+ }
394
+
395
+ /**
396
+ * @param {MongoDB} driver
397
+ *
398
+ *
399
+ * @returns {db_col["add_product_to_collection"]}
400
+ */
401
+ const add_product_to_collection = (driver) => {
402
+ return async (product_id_or_handle, collection_handle_or_id) => {
403
+
404
+ const coll = await driver.resources.collections._col.findOne(
405
+ handle_or_id(collection_handle_or_id)
406
+ );
407
+
408
+ if(!coll)
409
+ return;
410
+
411
+ const objid = to_objid(coll.id);
412
+
413
+ await update_specific_connection_of_relation_with_filter(
414
+ driver, 'products', 'collections',
415
+ handle_or_id(product_id_or_handle),
416
+ objid, coll, undefined,
417
+ [
418
+ `col:${coll.handle}`, `col:${coll.id}`
419
+ ]
420
+ );
421
+
422
+ }
423
+ }
424
+
425
+
426
+ /**
427
+ * @param {MongoDB} driver
428
+ *
429
+ *
430
+ * @returns {db_col["remove_product_from_collection"]}
431
+ */
432
+ const remove_product_from_collection = (driver) => {
433
+ return async (product_id_or_handle, collection_handle_or_id) => {
434
+
435
+ const coll = await driver.resources.collections._col.findOne(
436
+ handle_or_id(collection_handle_or_id)
437
+ );
438
+
439
+ if(!coll)
440
+ return;
441
+
442
+ const objid = to_objid(coll.id);
443
+
444
+ await remove_specific_connection_of_relation_with_filter(
445
+ driver, 'products', 'collections',
446
+ handle_or_id(product_id_or_handle),
447
+ objid, undefined,
448
+ [
449
+ `col:${coll.handle}`, `col:${coll.id}`
450
+ ]
451
+ );
452
+ }
453
+ }
454
+
455
+ /**
456
+ * @param {MongoDB} driver
457
+ *
458
+ *
459
+ * @returns {db_col["changeStockOfBy"]}
460
+ */
461
+ const changeStockOfBy = (driver) => {
462
+ return async (product_ids_or_handles, deltas) => {
463
+
464
+ /**
465
+ * @type {import('mongodb').AnyBulkWriteOperation<
466
+ * import('./utils.relations.js').WithRelations<
467
+ * import('@storecraft/core/v-api').ProductType |
468
+ * import('@storecraft/core/v-api').VariantType
469
+ * >
470
+ * >[]}
471
+ */
472
+ let ops = []
473
+
474
+ product_ids_or_handles.forEach(
475
+ (id, ix) => {
476
+ ops.push(
477
+ {
478
+ updateOne: {
479
+ filter: handle_or_id(id),
480
+ update: {
481
+ $inc: { qty: Math.round(deltas[ix]) }
482
+ }
483
+ }
484
+ }
485
+ );
486
+
487
+ ops.push(
488
+ {
489
+ updateOne: {
490
+ filter: handle_or_id(id),
491
+ update: {
492
+ $max: { qty: 0 }
493
+ }
494
+ }
495
+ }
496
+ );
497
+
498
+ }
499
+ );
500
+
501
+ if(!ops.length)
502
+ return;
503
+
504
+ await driver.resources.products._col.bulkWrite(
505
+ ops
506
+ );
507
+
508
+ }
509
+ }
510
+
511
+
512
+ /**
513
+ * @param {MongoDB} driver
514
+ *
515
+ *
516
+ * @return {db_col & { _col: ReturnType<col> }}
517
+ */
518
+ export const impl = (driver) => {
519
+
520
+ return {
521
+ _col: col(driver),
522
+ changeStockOfBy: changeStockOfBy(driver),
523
+ get: get(driver),
524
+ getBulk: get_bulk(driver, col(driver)),
525
+ upsert: upsert(driver),
526
+ remove: remove(driver),
527
+ list: list(driver),
528
+ add_product_to_collection: add_product_to_collection(driver),
529
+ remove_product_from_collection: remove_product_from_collection(driver),
530
+ list_product_collections: list_product_collections(driver),
531
+ list_product_variants: list_product_variants(driver),
532
+ list_related_products: list_related_products(driver),
533
+ list_product_discounts: list_product_discounts(driver),
534
+ count: count(driver)
535
+ }
536
+ }
537
+
@@ -0,0 +1,162 @@
1
+ import { MongoDB } from '../index.js'
2
+ import { query_to_mongo } from './utils.query.js';
3
+
4
+ /**
5
+ * @typedef {import('@storecraft/core/v-database').search} db_col
6
+ */
7
+
8
+
9
+
10
+ /**
11
+ * @type {(keyof import('@storecraft/core/v-database').db_driver["resources"])[]}
12
+ */
13
+ const tables = [
14
+ 'tags',
15
+ 'collections',
16
+ 'customers',
17
+ 'products',
18
+ 'storefronts',
19
+ 'images',
20
+ 'posts',
21
+ 'shipping_methods',
22
+ 'notifications',
23
+ 'discounts',
24
+ 'orders',
25
+ 'templates'
26
+ ]
27
+
28
+ /**
29
+ * @type {Record<string, keyof import('@storecraft/core/v-database').db_driver["resources"]>}
30
+ */
31
+ const prefix_to_resource = {
32
+ 'au': 'auth_users',
33
+ 'col': 'collections',
34
+ 'cus': 'customers',
35
+ 'dis': 'discounts',
36
+ 'img': 'images',
37
+ 'not': 'notifications',
38
+ 'order': 'orders',
39
+ 'pr': 'products',
40
+ 'ship': 'shipping_methods',
41
+ 'sf': 'storefronts',
42
+ 'tag': 'tags',
43
+ 'template': 'templates',
44
+ 'post': 'posts',
45
+
46
+ }
47
+
48
+ /**
49
+ *
50
+ * @param {string} id
51
+ *
52
+ * @returns {keyof import('@storecraft/core/v-database').db_driver["resources"]}
53
+ */
54
+ export const id_to_resource = id => {
55
+ let result = undefined;
56
+ try {
57
+ const prefix = id.split('_').at(0);
58
+ result = prefix_to_resource[prefix];
59
+ } catch(e) {
60
+
61
+ } finally {
62
+ return result;
63
+ }
64
+ }
65
+
66
+ /**
67
+ * @param {MongoDB} driver
68
+ *
69
+ *
70
+ * @returns {db_col["quicksearch"]}
71
+ */
72
+ export const quicksearch = (driver) => {
73
+ return async (query) => {
74
+
75
+ const { filter, sort, reverse_sign } = query_to_mongo(query);
76
+ const expand = query.expand ?? ['*'];
77
+ const tables_filtered = tables.filter(t => expand.includes('*') || expand.includes(t));
78
+
79
+ if(tables_filtered.length==0)
80
+ return {};
81
+
82
+ const pipeline = [
83
+ {
84
+ "$match": filter
85
+ },
86
+ {
87
+ $sort: sort
88
+ },
89
+ {
90
+ $limit: query.limit ?? 5
91
+ },
92
+ {
93
+ $project: {
94
+ title: 1,
95
+ handle: 1,
96
+ // '_relations.search': 1,
97
+ id: 1,
98
+ _id: 0
99
+ }
100
+ }
101
+ ];
102
+
103
+ const db = driver.mongo_client.db();
104
+
105
+ /** @type {import('@storecraft/core/v-api').QuickSearchResource[]} */
106
+ const items = await db.collection(tables_filtered[0]).aggregate(
107
+ [
108
+ ...pipeline,
109
+ ...tables_filtered.slice(1).map(
110
+ t => (
111
+ {
112
+ $unionWith: {
113
+ coll: t,
114
+ pipeline: pipeline
115
+ }
116
+ }
117
+ )
118
+ )
119
+ ],
120
+ {
121
+
122
+ }
123
+ ).toArray();
124
+
125
+
126
+ /** @type {import('@storecraft/core/v-api').QuickSearchResult} */
127
+ const result = {};
128
+
129
+ items.reduce(
130
+ (p, c) => {
131
+ const resource = id_to_resource(c.id);
132
+ const resource_meta = p[resource];
133
+
134
+ if(resource_meta) {
135
+ resource_meta.push(c);
136
+ } else {
137
+ p[resource] = [
138
+ c
139
+ ]
140
+ }
141
+
142
+ return p;
143
+ },
144
+ result
145
+ );
146
+
147
+ return result;
148
+ }
149
+ }
150
+
151
+ /**
152
+ * @param {MongoDB} driver
153
+ *
154
+ *
155
+ * @return {db_col}
156
+ * */
157
+ export const impl = (driver) => {
158
+
159
+ return {
160
+ quicksearch: quicksearch(driver)
161
+ }
162
+ }