@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 CHANGED
@@ -8,6 +8,7 @@
8
8
  [![MongoDB](https://github.com/store-craft/storecraft/actions/workflows/test.database-mongodb.yml/badge.svg)](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, migrateToLatest } from '@storecraft/database-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.11",
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",
@@ -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) {
@@ -12,7 +12,6 @@ import { query_to_mongo } from './utils.query.js';
12
12
  /**
13
13
  * @param {MongoDB} d
14
14
  *
15
- *
16
15
  * @returns {Collection<db_col["$type_get"]>}
17
16
  */
18
17
  const col = (d) => d.collection('customers');
@@ -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
- const cast = filter.value ?? [];
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
- const cast = filter.value ?? [];
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
- const cast = filter.value ?? [];
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
- const cast = filter.value ?? [];
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
- const cast = filter.value ?? [];
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
- const cast = filter.value ?? [];
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
- const cast = {
113
- from: 0,
114
- to: Number.POSITIVE_INFINITY,
115
- ...(filter?.value ?? {})
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);
@@ -42,6 +42,7 @@ const upsertBulk = (driver) => {
42
42
  items = items.map(item => ({...item}));
43
43
  items.forEach(
44
44
  (item, ix) => {
45
+ // @ts-ignore
45
46
  item._id = to_objid(item.id);
46
47
 
47
48
  add_search_terms_relation_on(
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
- /** @type {import('@storecraft/core/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
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').idable} T
198
- * @template {import('@storecraft/core/api').idable} G
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
- // console.log('filter', JSON.stringify(filter, null, 2))
279
+ console.log('filter', JSON.stringify(filter, null, 2))
282
280
  // console.log('sort', sort)
283
281
  // console.log('expand', query?.expand)
284
282
 
@@ -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<string>} c a cursor record
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
+ }