@storecraft/database-mongodb 1.0.11 → 1.0.13
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 +64 -1
- package/package.json +10 -1
- package/src/con.collections.js +2 -0
- package/src/con.customers.js +0 -1
- package/src/con.discounts.utils.js +25 -18
- package/src/con.notifications.js +1 -0
- package/src/con.search.js +18 -17
- package/src/con.shared.js +4 -6
- package/src/utils.query.js +2 -2
- package/vector-search-extension.js +110 -0
package/README.md
CHANGED
@@ -8,6 +8,7 @@
|
|
8
8
|
[](https://github.com/store-craft/storecraft/actions/workflows/test.database-mongodb.yml)
|
9
9
|
|
10
10
|
Official `mongodb` driver for `StoreCraft` on **Node.js** / **Deno** / **Bun** platforms.
|
11
|
+
Also, an example of [Semantic Vector Search](https://www.mongodb.com/developer/products/atlas/semantic-search-mongodb-atlas-vector-search/) extension at `@storecraft/database-mongodb/vector-search-extension`
|
11
12
|
|
12
13
|
```bash
|
13
14
|
npm i @storecraft/database-mongodb
|
@@ -20,8 +21,10 @@ import 'dotenv/config';
|
|
20
21
|
import http from "node:http";
|
21
22
|
import { App } from '@storecraft/core'
|
22
23
|
import { NodePlatform } from '@storecraft/core/platform/node';
|
23
|
-
import { MongoDB
|
24
|
+
import { MongoDB } from '@storecraft/database-mongodb'
|
25
|
+
import { migrateToLatest } from '@storecraft/database-mongodb/migrate.js'
|
24
26
|
import { NodeLocalStorage } from '@storecraft/core/storage/node'
|
27
|
+
import { MongoVectorSearch } from '@storecraft/database-mongodb/vector-search-extension'
|
25
28
|
|
26
29
|
const app = new App(
|
27
30
|
{
|
@@ -32,6 +35,9 @@ const app = new App(
|
|
32
35
|
)
|
33
36
|
.withPlatform(new NodePlatform())
|
34
37
|
.withDatabase(new MongoDB({ db_name: 'test', url: '...', options: {}}))
|
38
|
+
.withExtensions(
|
39
|
+
'mongo-vector-search': new MongoVectorSearch({ openai_key: process.env.OPENAI })
|
40
|
+
)
|
35
41
|
|
36
42
|
await app.init();
|
37
43
|
await migrateToLatest(app.db, false);
|
@@ -45,6 +51,63 @@ const server = http.createServer(app.handler).listen(
|
|
45
51
|
|
46
52
|
```
|
47
53
|
|
54
|
+
## (Recommended) setup semantic/ai vector search extension for products
|
55
|
+
|
56
|
+
1. in [Atlas](https://cloud.mongodb.com/) dashboard, create a vector index (call it `vector_index`) for `products` collection:
|
57
|
+
|
58
|
+
```json
|
59
|
+
{
|
60
|
+
"fields": [
|
61
|
+
{
|
62
|
+
"numDimensions": 1536,
|
63
|
+
"path": "embedding",
|
64
|
+
"similarity": "cosine",
|
65
|
+
"type": "vector"
|
66
|
+
}
|
67
|
+
]
|
68
|
+
}
|
69
|
+
```
|
70
|
+
|
71
|
+
2. Now, every upserted product will be eligible for semantic search by it's title + description.
|
72
|
+
3. The extension is publicly available via HTTP (`POST` request)
|
73
|
+
```js
|
74
|
+
await fetch(
|
75
|
+
'http://localhost:8000/api/extensions/mongo-vector-search/search',
|
76
|
+
{
|
77
|
+
method: 'POST',
|
78
|
+
body: JSON.stringify(
|
79
|
+
{
|
80
|
+
query: 'I am interested in Nintendo related clothing, such as shirts',
|
81
|
+
limit: 1
|
82
|
+
}
|
83
|
+
)
|
84
|
+
}
|
85
|
+
)
|
86
|
+
```
|
87
|
+
|
88
|
+
returns `ProductType[]` array
|
89
|
+
|
90
|
+
```json
|
91
|
+
[
|
92
|
+
{
|
93
|
+
"title": "Super Mario T Shirt",
|
94
|
+
"handle": "super-mario-t-shirt",
|
95
|
+
"description": "This Super mario shirt is XL size and
|
96
|
+
features a colorful print of Lugi and Mario.",
|
97
|
+
"media": [
|
98
|
+
"storage://images/super-mario-shirt_1738686680944_w_819_h_460.jpeg"
|
99
|
+
],
|
100
|
+
"price": 100,
|
101
|
+
"qty": 1,
|
102
|
+
"active": true,
|
103
|
+
"id": "pr_67a240e4000000d34bcf0743",
|
104
|
+
"created_at": "2025-02-04T16:31:32.909Z",
|
105
|
+
"updated_at": "2025-02-04T16:58:25.286Z"
|
106
|
+
}
|
107
|
+
]
|
108
|
+
```
|
109
|
+
|
110
|
+
|
48
111
|
```text
|
49
112
|
Author: Tomer Shalev <tomer.shalev@gmail.com>
|
50
113
|
```
|
package/package.json
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
{
|
2
2
|
"name": "@storecraft/database-mongodb",
|
3
|
-
"version": "1.0.
|
3
|
+
"version": "1.0.13",
|
4
4
|
"description": "Storecraft database driver for mongodb on node / bun / deno platform",
|
5
5
|
"license": "MIT",
|
6
6
|
"author": "Tomer Shalev (https://github.com/store-craft)",
|
@@ -19,6 +19,15 @@
|
|
19
19
|
"type": "module",
|
20
20
|
"main": "index.js",
|
21
21
|
"types": "./types.public.d.ts",
|
22
|
+
"exports": {
|
23
|
+
".": {
|
24
|
+
"import": "./index.js",
|
25
|
+
"types": "./types.public.d.ts"
|
26
|
+
},
|
27
|
+
"./*": {
|
28
|
+
"import": "./*"
|
29
|
+
}
|
30
|
+
},
|
22
31
|
"scripts": {
|
23
32
|
"database-mongodb:test": "node ./tests/runner.test.js",
|
24
33
|
"test": "npm run database-mongodb:test",
|
package/src/con.collections.js
CHANGED
@@ -73,6 +73,7 @@ const upsert = (driver) => {
|
|
73
73
|
driver, 'collections', objid, data, session
|
74
74
|
);
|
75
75
|
|
76
|
+
// @ts-ignore
|
76
77
|
}, transactionOptions
|
77
78
|
);
|
78
79
|
|
@@ -136,6 +137,7 @@ const remove = (driver) => {
|
|
136
137
|
driver, 'collections', objid, session
|
137
138
|
);
|
138
139
|
|
140
|
+
// @ts-ignore
|
139
141
|
}, transactionOptions
|
140
142
|
);
|
141
143
|
} catch(e) {
|
package/src/con.customers.js
CHANGED
@@ -40,8 +40,9 @@ export const discount_to_mongo_conjunctions = d => {
|
|
40
40
|
break;
|
41
41
|
case enums.FilterMetaEnum.p_in_products.op:
|
42
42
|
{
|
43
|
-
/** @type {import("@storecraft/core/api").FilterValue_p_in_products} */
|
44
|
-
|
43
|
+
const cast = /** @type {import("@storecraft/core/api").FilterValue_p_in_products} */ (
|
44
|
+
filter.value ?? []
|
45
|
+
);
|
45
46
|
|
46
47
|
conjunctions.push(
|
47
48
|
{ handle: { $in: cast.map(it => it.handle) } }
|
@@ -50,8 +51,9 @@ export const discount_to_mongo_conjunctions = d => {
|
|
50
51
|
break;
|
51
52
|
case enums.FilterMetaEnum.p_not_in_products.op:
|
52
53
|
{
|
53
|
-
/** @type {import("@storecraft/core/api").FilterValue_p_not_in_products} */
|
54
|
-
|
54
|
+
const cast = /** @type {import("@storecraft/core/api").FilterValue_p_not_in_products} */(
|
55
|
+
filter.value ?? []
|
56
|
+
);
|
55
57
|
|
56
58
|
conjunctions.push(
|
57
59
|
{ handle: { $nin: cast.map(it => it.handle) } }
|
@@ -60,8 +62,9 @@ export const discount_to_mongo_conjunctions = d => {
|
|
60
62
|
break;
|
61
63
|
case enums.FilterMetaEnum.p_in_tags.op:
|
62
64
|
{
|
63
|
-
/** @type {import("@storecraft/core/api").FilterValue_p_in_tags} */
|
64
|
-
|
65
|
+
const cast = /** @type {import("@storecraft/core/api").FilterValue_p_in_tags} */ (
|
66
|
+
filter.value ?? []
|
67
|
+
);
|
65
68
|
|
66
69
|
conjunctions.push(
|
67
70
|
{ tags: { $in: cast } }
|
@@ -70,8 +73,9 @@ export const discount_to_mongo_conjunctions = d => {
|
|
70
73
|
break;
|
71
74
|
case enums.FilterMetaEnum.p_not_in_tags.op:
|
72
75
|
{
|
73
|
-
/** @type {import("@storecraft/core/api").FilterValue_p_not_in_tags} */
|
74
|
-
|
76
|
+
const cast = /** @type {import("@storecraft/core/api").FilterValue_p_not_in_tags} */(
|
77
|
+
filter.value ?? []
|
78
|
+
);
|
75
79
|
|
76
80
|
conjunctions.push(
|
77
81
|
{ tags: { $nin: cast } }
|
@@ -80,8 +84,9 @@ export const discount_to_mongo_conjunctions = d => {
|
|
80
84
|
break;
|
81
85
|
case enums.FilterMetaEnum.p_in_collections.op:
|
82
86
|
{
|
83
|
-
/** @type {import("@storecraft/core/api").FilterValue_p_in_collections} */
|
84
|
-
|
87
|
+
const cast = /** @type {import("@storecraft/core/api").FilterValue_p_in_collections} */ (
|
88
|
+
filter.value ?? []
|
89
|
+
);
|
85
90
|
|
86
91
|
conjunctions.push(
|
87
92
|
{
|
@@ -94,8 +99,9 @@ export const discount_to_mongo_conjunctions = d => {
|
|
94
99
|
break;
|
95
100
|
case enums.FilterMetaEnum.p_not_in_collections.op:
|
96
101
|
{
|
97
|
-
/** @type {import("@storecraft/core/api").FilterValue_p_not_in_collections} */
|
98
|
-
|
102
|
+
const cast = /** @type {import("@storecraft/core/api").FilterValue_p_not_in_collections} */ (
|
103
|
+
filter.value ?? []
|
104
|
+
);
|
99
105
|
|
100
106
|
conjunctions.push(
|
101
107
|
{
|
@@ -108,12 +114,13 @@ export const discount_to_mongo_conjunctions = d => {
|
|
108
114
|
break;
|
109
115
|
case enums.FilterMetaEnum.p_in_price_range.op:
|
110
116
|
{
|
111
|
-
/** @type {import("@storecraft/core/api").FilterValue_p_in_price_range} */
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
+
const cast = /** @type {import("@storecraft/core/api").FilterValue_p_in_price_range} */ (
|
118
|
+
{
|
119
|
+
from: 0,
|
120
|
+
to: Number.POSITIVE_INFINITY,
|
121
|
+
...(filter?.value ?? {})
|
122
|
+
}
|
123
|
+
);
|
117
124
|
|
118
125
|
const from = extract_abs_number(cast.from);
|
119
126
|
const to = extract_abs_number(cast.to);
|
package/src/con.notifications.js
CHANGED
package/src/con.search.js
CHANGED
@@ -102,25 +102,26 @@ export const quicksearch = (driver) => {
|
|
102
102
|
|
103
103
|
const db = driver.mongo_client.db(driver.name);
|
104
104
|
|
105
|
-
|
106
|
-
const items =
|
107
|
-
[
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
105
|
+
|
106
|
+
const items = /** @type {import('@storecraft/core/api').QuickSearchResource[]} */ (
|
107
|
+
await db.collection(tables_filtered[0]).aggregate(
|
108
|
+
[
|
109
|
+
...pipeline,
|
110
|
+
...tables_filtered.slice(1).map(
|
111
|
+
t => (
|
112
|
+
{
|
113
|
+
$unionWith: {
|
114
|
+
coll: t,
|
115
|
+
pipeline: pipeline
|
116
|
+
}
|
115
117
|
}
|
116
|
-
|
118
|
+
)
|
117
119
|
)
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
).toArray();
|
120
|
+
],
|
121
|
+
{
|
122
|
+
}
|
123
|
+
).toArray()
|
124
|
+
);
|
124
125
|
|
125
126
|
|
126
127
|
/** @type {import('@storecraft/core/api').QuickSearchResult} */
|
package/src/con.shared.js
CHANGED
@@ -11,15 +11,13 @@ import { add_search_terms_relation_on } from './utils.relations.js'
|
|
11
11
|
* @template {import('@storecraft/core/api').BaseType} T
|
12
12
|
* @template {import('@storecraft/core/api').BaseType} G
|
13
13
|
*
|
14
|
-
*
|
15
14
|
* @param {MongoDB} driver
|
16
15
|
* @param {Collection<G>} col
|
17
16
|
*
|
18
|
-
*
|
19
17
|
* @returns {import('@storecraft/core/database').db_crud<T, G>["upsert"]}
|
20
|
-
*
|
21
18
|
*/
|
22
19
|
export const upsert_regular = (driver, col) => {
|
20
|
+
|
23
21
|
return async (data, search_terms=[]) => {
|
24
22
|
|
25
23
|
data = {...data};
|
@@ -194,8 +192,8 @@ export const get_regular = (driver, col) => {
|
|
194
192
|
* should be instead
|
195
193
|
*
|
196
194
|
*
|
197
|
-
* @template {import('@storecraft/core/api').
|
198
|
-
* @template {import('@storecraft/core/api').
|
195
|
+
* @template {import('@storecraft/core/api').withOptionalID} T
|
196
|
+
* @template {import('@storecraft/core/api').withOptionalID} G
|
199
197
|
*
|
200
198
|
*
|
201
199
|
* @param {MongoDB} driver
|
@@ -278,7 +276,7 @@ export const list_regular = (driver, col) => {
|
|
278
276
|
|
279
277
|
// console.log('reverse_sign', reverse_sign)
|
280
278
|
// console.log('query', query)
|
281
|
-
|
279
|
+
console.log('filter', JSON.stringify(filter, null, 2))
|
282
280
|
// console.log('sort', sort)
|
283
281
|
// console.log('expand', query?.expand)
|
284
282
|
|
package/src/utils.query.js
CHANGED
@@ -116,7 +116,7 @@ export const query_vql_to_mongo = root => {
|
|
116
116
|
* Let's transform ids into mongo ids
|
117
117
|
*
|
118
118
|
*
|
119
|
-
* @param {import("@storecraft/core/api").Tuple
|
119
|
+
* @param {import("@storecraft/core/api").Tuple<>} c a cursor record
|
120
120
|
*
|
121
121
|
*
|
122
122
|
* @returns {[k: string, v: any]}
|
@@ -124,7 +124,7 @@ export const query_vql_to_mongo = root => {
|
|
124
124
|
const transform = c => {
|
125
125
|
if(c[0]!=='id')
|
126
126
|
return c;
|
127
|
-
return [ '_id', to_objid(c[1]) ];
|
127
|
+
return [ '_id', to_objid(String(c[1])) ];
|
128
128
|
}
|
129
129
|
|
130
130
|
/**
|
@@ -0,0 +1,110 @@
|
|
1
|
+
/**
|
2
|
+
* @import {extension} from '@storecraft/core/extensions'
|
3
|
+
*/
|
4
|
+
import { MongoDB } from './index.js';
|
5
|
+
/**
|
6
|
+
*
|
7
|
+
* @typedef {object} Config
|
8
|
+
* @property {string} openai_key OpenAI key
|
9
|
+
*/
|
10
|
+
|
11
|
+
/**
|
12
|
+
* @implements {extension<Config>}
|
13
|
+
*/
|
14
|
+
export class MongoVectorSearch {
|
15
|
+
/** @param {Config} config */
|
16
|
+
constructor(config) {
|
17
|
+
this.config = config
|
18
|
+
}
|
19
|
+
|
20
|
+
info = {
|
21
|
+
name: 'Mongo Vector Search',
|
22
|
+
}
|
23
|
+
|
24
|
+
/** @type {extension["onInit"]} */
|
25
|
+
onInit = (app) => {
|
26
|
+
this.app = app;
|
27
|
+
|
28
|
+
app.pubsub.on(
|
29
|
+
'products/upsert',
|
30
|
+
async (evt) => {
|
31
|
+
const product = evt.payload.current;
|
32
|
+
// @ts-ignore
|
33
|
+
product.embedding = await embed_text(
|
34
|
+
`This product's title is ${product.title}, it's price is
|
35
|
+
${product.price} and has the following description
|
36
|
+
${product.description}`
|
37
|
+
);
|
38
|
+
console.log('Update Product ' + product.handle);
|
39
|
+
}
|
40
|
+
)
|
41
|
+
}
|
42
|
+
|
43
|
+
/** @type {extension["invokeAction"]} */
|
44
|
+
invokeAction = (handle) => {
|
45
|
+
|
46
|
+
if (handle==='search') {
|
47
|
+
/** @param {{query: string, limit:number}} args */
|
48
|
+
return async (args) => {
|
49
|
+
|
50
|
+
const db = (/** @type {MongoDB} */ (this.app.db));
|
51
|
+
|
52
|
+
return db.collection('products').aggregate(
|
53
|
+
[
|
54
|
+
{
|
55
|
+
"$vectorSearch": {
|
56
|
+
queryVector: await embed_text(
|
57
|
+
args.query, this.config.openai_key
|
58
|
+
),
|
59
|
+
path: "embedding",
|
60
|
+
exact: true,
|
61
|
+
limit: args.limit ?? 1,
|
62
|
+
index: "vector_index",
|
63
|
+
},
|
64
|
+
},
|
65
|
+
{
|
66
|
+
"$project": {
|
67
|
+
embedding: 0, _relations: 0, _id: 0
|
68
|
+
}
|
69
|
+
}
|
70
|
+
],
|
71
|
+
).toArray();
|
72
|
+
|
73
|
+
}
|
74
|
+
}
|
75
|
+
}
|
76
|
+
|
77
|
+
}
|
78
|
+
|
79
|
+
|
80
|
+
/**
|
81
|
+
*
|
82
|
+
* @param {string} text
|
83
|
+
* @param {string} openai_key
|
84
|
+
* @returns {Promise<number[]>} 1536 vector
|
85
|
+
*/
|
86
|
+
export const embed_text = async (text = 'hello', openai_key) => {
|
87
|
+
|
88
|
+
const r = await fetch(
|
89
|
+
'https://api.openai.com/v1/embeddings',
|
90
|
+
{
|
91
|
+
method: 'POST',
|
92
|
+
headers: {
|
93
|
+
'Authorization': `Bearer ${openai_key}`,
|
94
|
+
'Content-Type': 'application/json'
|
95
|
+
},
|
96
|
+
body: JSON.stringify(
|
97
|
+
{
|
98
|
+
input: text,
|
99
|
+
model: 'text-embedding-3-small',
|
100
|
+
encoding_format: 'float',
|
101
|
+
dimensions: 1536
|
102
|
+
}
|
103
|
+
)
|
104
|
+
}
|
105
|
+
);
|
106
|
+
|
107
|
+
const json = await r.json();
|
108
|
+
|
109
|
+
return json.data?.[0]?.embedding;
|
110
|
+
}
|