@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.
package/README.md ADDED
@@ -0,0 +1,50 @@
1
+ # Storecraft MongoDB driver for Node.js
2
+
3
+ <div style="text-align:center">
4
+ <img src='https://storecraft.app/storecraft-color.svg'
5
+ width='90%'' />
6
+ </div><hr/><br/>
7
+
8
+ Official `mongodb` driver for `StoreCraft` on **Node.js** platforms.
9
+
10
+ ```bash
11
+ npm i @storecraft/database-mongodb
12
+ ```
13
+
14
+ ## usage
15
+
16
+ ```js
17
+ import 'dotenv/config';
18
+ import http from "node:http";
19
+ import { join } from "node:path";
20
+ import { homedir } from "node:os";
21
+
22
+ import { App } from '@storecraft/core'
23
+ import { NodePlatform } from '@storecraft/platforms/node';
24
+ import { MongoDB } from '@storecraft/database-mongodb'
25
+ import { NodeLocalStorage } from '@storecraft/storage-local/node'
26
+
27
+ const app = new App(
28
+ {
29
+ auth_admins_emails: ['admin@sc.com'],
30
+ auth_secret_access_token: 'auth_secret_access_token',
31
+ auth_secret_refresh_token: 'auth_secret_refresh_token'
32
+ }
33
+ )
34
+ .withPlatform(new NodePlatform())
35
+ .withDatabase(new MongoDB({ db_name: 'test'}))
36
+
37
+ await app.init();
38
+
39
+ const server = http.createServer(app.handler).listen(
40
+ 8000,
41
+ () => {
42
+ console.log(`Server is running on http://localhost:8000`);
43
+ }
44
+ );
45
+
46
+ ```
47
+
48
+ ```text
49
+ Author: Tomer Shalev <tomer.shalev@gmail.com>
50
+ ```
package/db-strategy.md ADDED
@@ -0,0 +1,284 @@
1
+ # MongoDB data modeling
2
+
3
+ We embed some relation info in documents to model relations.
4
+ ```js
5
+ {
6
+ _relations: {
7
+ [collection_name]: {
8
+ ids: [ objectId(0), objectId(1)....],
9
+ entries: {
10
+ 0: {
11
+ ... data
12
+ },
13
+ 1: {
14
+ ... more data
15
+ }
16
+ }
17
+ }
18
+ }
19
+ }
20
+ ```
21
+
22
+ - `ids` array list all the ids of the related documents and
23
+ help with querying a relation if we need.
24
+ - `entries` are embedding of these documents keyed by id and value is a document, this
25
+ is used for super fast read.
26
+ - We use some of this relations to expand relations and return them to the user,
27
+ as long these relations are not too big. For example, product will have on avarage 2
28
+ related colletions, therefore we don't mind embedding the collections inside a product,
29
+ so when a user wants a product with expanded related collections, he will get them the fastest.
30
+ - In general we favour FAST READS over FAST WRITES.
31
+ - so product-->collections, we don't mind expanding.
32
+ - but, collection-->products, we do mind and will use a different strategy.
33
+ - In case, we have a large collection, we opt for querying.
34
+ For example a collection `shirts` can relate to 5000 products, in this case,
35
+ we offer a way to query the products constrained on collection name, which means, that
36
+ we don't expand as above and return everything but use a different pathway. We have many ways
37
+ to achieve this, either by the built in search terms array.
38
+
39
+
40
+ ## products --> collections
41
+ Each product has the following relation:
42
+ ```js
43
+ product._relations.collections: {
44
+ // object ids of related collections
45
+ ids: ObjectId[],
46
+ // the collections documents
47
+ entries: Record<ID_STRING, CollectionType>
48
+ }
49
+ ```
50
+
51
+ How connections intentions from product to collections are formed ?
52
+ Connections are given in the `product.collections = [ { id:'id1' }, ..]` property.
53
+ We use these as user intention to form a connection in the database. super convenient
54
+
55
+ `Product SAVE:`
56
+ - for each related collection add the entry `_relations.collections.entries[col-id]=col` in the product document.
57
+ - for each related collection add the collection `ObjectId` to array `_relations.collections.ids` in the product document.
58
+
59
+ `Product DELETE:`
60
+ - Nothing todo
61
+
62
+ `Collection SAVE:`
63
+ - update each related product document with `_relations.collections.entries[col-id] = c`
64
+
65
+ `Collection DELETE:`
66
+ - delete in each related product the entry `_relations.collections.entries[col-id]`
67
+ - remove in each related product the `ObjectId` from array `_relations.collections.ids`
68
+ - remove in each related product the `[col:col-handle, col:col-id` from array `search` in the products documents.
69
+
70
+
71
+ ## products --> products variants
72
+ Each product has the following relation:
73
+ ```js
74
+ product._relations.variants: {
75
+ // object ids of related collections
76
+ ids: ObjectId[],
77
+ // the variants documents
78
+ entries: Record<ID_STRING, ProductType>
79
+ }
80
+ ```
81
+
82
+ Variant is any product, that has
83
+ - `parent_handle` property
84
+ - `parent_id` property
85
+ - `variant_hint` property
86
+
87
+ How connections intentions from product to variants are formed ?
88
+ As opposed to `products <--> collections`, connections are made implicitly, because of
89
+ the async nature of connecting and creating variants. In the future, I will add possibility
90
+ to add variants like collections, i.e, explicitly.
91
+ - async a product is created with `parent_handle` property set.
92
+
93
+ Notes:
94
+ - Currently, I have no use for the `ids`, but it might change (because each sub variant has one parent).
95
+ - Variants are always created by a product, that specify he has a parent. i.e, we rely on
96
+ integrity, so use it with care.
97
+
98
+
99
+ `Variant SAVE`:
100
+ - save the product ofcourse
101
+ - use `parent_id` to:
102
+ - add variant `ObjectId` into the parent's `_relations.variants.ids`
103
+ - update variant document in the parent's `_relations.collections.entries[variant-id]=variant`
104
+
105
+ `Variant DELETE:`
106
+ - use `parent_id` to:
107
+ - delete in parent the entry `_relations.variants.entries[variant-id]`
108
+ - remove in parent the `ObjectId` from array `_relations.variants.ids`
109
+
110
+ `Product (parent) SAVE:`
111
+ - nothing, we don't support explicit relations yet
112
+
113
+ `Product (parent) DELETE:`
114
+ - delete all the children products
115
+ - delete the parent
116
+
117
+
118
+ ## products --> discounts
119
+ Again, a relation, that connects a collection with many entries (`products`) to one with
120
+ much fewer (`discounts`). Therefore, we embed documents in.
121
+
122
+ Each product has the following relation:
123
+ ```js
124
+ product._relations.discounts: {
125
+ // object ids of related discounts
126
+ ids: ObjectId[],
127
+ // the variants documents
128
+ entries: Record<ID_STRING, DiscountType>
129
+ }
130
+ ```
131
+
132
+ Notes:
133
+ - like `variants`, relation connections are made implictly.
134
+ - Everytime a discount is saved, it needs to form connection with eligible products.
135
+ - Everytime a product is saved, it has to remove all of it's discounts and re-test itself.
136
+
137
+
138
+ `Discount SAVE`:
139
+ - Remove discount info from every related product:
140
+ - remove discount `ObjectId` from the product's `_relations.discounts.ids` array
141
+ - unset discount document from the product's `_relations.discounts.entries[discount-id]`
142
+ - remove [`discount:discount-handle`, `discount:discount-id`] from the product's `search`
143
+
144
+ - query the eligible products of the discount:
145
+ - for each eligible product:
146
+ - add discount `ObjectId` into the product's `_relations.discounts.ids` array
147
+ - update discount document in the product's `_relations.discounts.entries[discount-id]=discount`
148
+ - add [`discount:discount-handle`, `discount:discount-id`] to the product's `search`
149
+
150
+ `Discount DELETE`:
151
+ - Remove discount info from every related product:
152
+ - remove discount `ObjectId` from the product's `_relations.discounts.ids` array
153
+ - unset discount document from the product's `_relations.discounts.entries[discount-id]`
154
+ - remove [`discount:discount-handle`, `discount:discount-id`] from the product's `search`
155
+ - remove discount document
156
+
157
+
158
+ `Product SAVE:`
159
+ Product might have changed, therefore it needs to be re-tested for discounts, BUT,
160
+ it is not OKAY to punish all products saves, usually, it is only required if
161
+ tags/collections/price changes
162
+ - delete the product's self relations `_relations.discounts`
163
+ - get all discounts of type product, which are active and for each:
164
+ - test eligibility for discount locally,
165
+ - If eligible:
166
+ - add discount `ObjectId` into the product's `_relations.discounts.ids` array
167
+ - update discount document in the product's `_relations.discounts.entries[discount-id]=discount`
168
+ - add [`discount:discount-handle`, `discount:discount-id`] into the product's `search`
169
+
170
+ `Product DELETE:`
171
+ - Do nothing
172
+
173
+
174
+ ## storefronts --> products, collections, discounts, shipping, posts
175
+ We are going to create explicit connections, (the way `product` connects `collections`).
176
+
177
+ Each product has the following relation:
178
+ ```js
179
+ storefront._relations: {
180
+ products: {
181
+ ids: ObjectId[],
182
+ entries: Record<ID_STRING, ProductType>
183
+ },
184
+ collections: {
185
+ ids: ObjectId[],
186
+ entries: Record<ID_STRING, CollectionType>
187
+ },
188
+ discounts: {
189
+ ids: ObjectId[],
190
+ entries: Record<ID_STRING, DiscountType>
191
+ },
192
+ shipping: {
193
+ ids: ObjectId[],
194
+ entries: Record<ID_STRING, ShippingMethodType>
195
+ },
196
+ posts: {
197
+ ids: ObjectId[],
198
+ entries: Record<ID_STRING, PostType>
199
+ },
200
+ }
201
+ ```
202
+
203
+ `Storefront SAVE:`
204
+ - For Each product in `storefront.products`:
205
+ - Add `_relations.products.entries[product-id]=product`.
206
+ - Add `ObjectId` to `_relations.products.ids`.
207
+
208
+ - For Each collection in `storefront.collections`:
209
+ - Add `_relations.collections.entries[collection-id]=collection`.
210
+ - Add `ObjectId` to `_relations.collections.ids`.
211
+
212
+ - For Each discount in `storefront.discounts`:
213
+ - Add `_relations.discounts.entries[discount-id]=discount`.
214
+ - Add `ObjectId` to `_relations.discounts.ids`.
215
+
216
+ - For Each shipping method in `storefront.shipping_methods`:
217
+ - Add `_relations.shipping_methods.entries[shipping_method-id]=shipping_method`.
218
+ - Add `ObjectId` to `_relations.shipping_methods.ids`.
219
+
220
+ - For Each post in `storefront.posts`:
221
+ - Add `_relations.posts.entries[post-id]=post`.
222
+ - Add `ObjectId` to `_relations.posts.ids`.
223
+
224
+ `Storefront DELETE:`
225
+ - Nothing todo
226
+
227
+ `product/collection/discount/shipping/post SAVE:`
228
+ - update each related `storefront` document with `storefront._relations.products.entries[product-id] = product`
229
+ - update each related `storefront` document with `storefront._relations.collections.entries[collection-id] = collection`
230
+ - update each related `storefront` document with `storefront._relations.discounts.entries[discount-id] = discount`
231
+ - update each related `storefront` document with `storefront._relations.shipping_methods.entries[shipping_method-id] = shipping_method`
232
+ - update each related `storefront` document with `storefront._relations.posts.entries[post-id] = post`
233
+
234
+
235
+ `product/collection/discount/shipping/post DELETE:`
236
+ - on `product delete`:
237
+ - delete in each related `storefront` the entry `storefront._relations.products.entries[product-id]`
238
+ - remove in each related `storefront` the `ObjectId` from array `storefront._relations.products.ids`
239
+ - on `collection delete`:
240
+ - delete in each related `storefront` the entry `storefront._relations.collections.entries[collection-id]`
241
+ - remove in each related `storefront` the `ObjectId` from array `storefront._relations.collections.ids`
242
+ - on `discount delete`:
243
+ - delete in each related `storefront` the entry `storefront._relations.discounts.entries[discount-id]`
244
+ - remove in each related `storefront` the `ObjectId` from array `storefront._relations.discounts.ids`
245
+ - on `shipping_method delete`:
246
+ - delete in each related `storefront` the entry `storefront._relations.shipping_methods.entries[shipping_method-id]`
247
+ - remove in each related `storefront` the `ObjectId` from array `storefront._relations.shipping_methods.ids`
248
+ - on `post delete`:
249
+ - delete in each related `storefront` the entry `storefront._relations.posts.entries[post-id]`
250
+ - remove in each related `storefront` the `ObjectId` from array `storefront._relations.posts.ids`
251
+
252
+
253
+
254
+ ## images
255
+ images are immutable, can only be created or deleted. They are not normalized in places used.
256
+
257
+ **products/collections/discounts/posts/shipping/storefronts**.{media} --> `image.url`
258
+
259
+ Some collections have `media[]` array, and we use it to keep track of used images in
260
+ the system, it is a soft feature. no hard relations on them.
261
+
262
+ `image DELETE`
263
+ - remove from storage if it's there
264
+ - for each `products/collections/discounts/posts/shipping/storefronts`, that has image url
265
+ in it's `media` array, simply remove it.
266
+
267
+ `products/collections/discounts/posts/shipping/storefronts GET`:
268
+ - fetch storage settings
269
+ - for each media url entry, if it has `storage://`, rewrite it for CDN setting
270
+
271
+ `products/collections/discounts/posts/shipping/storefronts SAVE`:
272
+ - for each media url entry:
273
+ - upsert `image` record with the url
274
+
275
+ ## customers --> auth_user
276
+ they are related through `customer.auth_id` field
277
+
278
+ * `au_{id} == cus_{id}`, the postfix is the same
279
+
280
+ `customer DELETE:`
281
+ - delete the auth user references by `customer.auth_id`
282
+
283
+ `auth_user DELETE:`
284
+ - do nothing
package/index.js ADDED
@@ -0,0 +1,193 @@
1
+ import { App } from '@storecraft/core';
2
+ import { Collection, MongoClient, ServerApiVersion } from 'mongodb';
3
+ import { impl as auth_users } from './src/con.auth_users.js';
4
+ import { impl as collections } from './src/con.collections.js';
5
+ import { impl as customers } from './src/con.customers.js';
6
+ import { impl as discounts } from './src/con.discounts.js';
7
+ import { impl as images } from './src/con.images.js';
8
+ import { impl as notifications } from './src/con.notifications.js';
9
+ import { impl as orders } from './src/con.orders.js';
10
+ import { impl as posts } from './src/con.posts.js';
11
+ import { impl as products } from './src/con.products.js';
12
+ import { impl as shipping } from './src/con.shipping.js';
13
+ import { impl as storefronts } from './src/con.storefronts.js';
14
+ import { impl as tags } from './src/con.tags.js';
15
+ import { impl as templates } from './src/con.templates.js';
16
+ import { impl as search } from './src/con.search.js';
17
+ export { migrateToLatest } from './migrate.js';
18
+
19
+ /**
20
+ * @typedef {Partial<import('./types.public.d.ts').Config>} Config
21
+ */
22
+
23
+ /**
24
+ *
25
+ * @param {string} uri
26
+ * @param {import('mongodb').MongoClientOptions} [options]
27
+ */
28
+ const connect = async (uri, options) => {
29
+
30
+ options = options ?? {
31
+ ignoreUndefined: true,
32
+ serverApi: {
33
+ version: ServerApiVersion.v1,
34
+ strict: true,
35
+ deprecationErrors: true,
36
+
37
+ }
38
+ }
39
+ const client = new MongoClient(uri, options);
40
+
41
+ return client.connect();
42
+ }
43
+
44
+ /**
45
+ * @typedef {import('@storecraft/core/v-database').db_driver} db_driver
46
+ *
47
+ *
48
+ * @implements {db_driver}
49
+ */
50
+ export class MongoDB {
51
+
52
+ /**
53
+ *
54
+ * @type {boolean}
55
+ */
56
+ #_is_ready;
57
+
58
+ /**
59
+ *
60
+ * @type {App<any, any, any>}
61
+ */
62
+ #_app;
63
+
64
+ /**
65
+ *
66
+ * @type {MongoClient}
67
+ */
68
+ #_mongo_client;
69
+
70
+ /**
71
+ *
72
+ * @type {Config}
73
+ */
74
+ #_config;
75
+
76
+ // /** @type {db_driver["resources"]} */
77
+ // #_resources;
78
+
79
+ /**
80
+ *
81
+ * @param {Config} [config] config, if undefined,
82
+ * env variables `MONGODB_URL` will be used for uri upon init later
83
+ */
84
+ constructor(config) {
85
+ this.#_is_ready = false;
86
+ this.#_config = config;
87
+ }
88
+
89
+ /**
90
+ *
91
+ * @param {App} app
92
+ *
93
+ *
94
+ * @returns {Promise<this>}
95
+ */
96
+ async init(app) {
97
+ if(this.isReady)
98
+ return this;
99
+ const c = this.#_config;
100
+ this.#_config = {
101
+ ...c,
102
+ url: c?.url ?? app.platform.env.MONGODB_URL,
103
+ db_name: c?.db_name ?? app.platform.env.MONGODB_NAME ?? 'main'
104
+ }
105
+
106
+ this.#_mongo_client = await connect(
107
+ this.config.url,
108
+ this.config.options
109
+ );
110
+
111
+ this.#_app = app;
112
+
113
+ this.resources = {
114
+ auth_users: auth_users(this),
115
+ collections: collections(this),
116
+ customers: customers(this),
117
+ discounts: discounts(this),
118
+ images: images(this),
119
+ notifications: notifications(this),
120
+ orders: orders(this),
121
+ posts: posts(this),
122
+ products: products(this),
123
+ storefronts: storefronts(this),
124
+ tags: tags(this),
125
+ shipping_methods: shipping(this),
126
+ templates: templates(this),
127
+ search: search(this),
128
+ }
129
+
130
+ this.#_is_ready = true;
131
+
132
+ return this;
133
+ }
134
+
135
+ async disconnect() {
136
+ await this.mongo_client.close(true);
137
+ return true;
138
+ }
139
+
140
+ get isReady() { return this.#_is_ready; }
141
+
142
+ throwIfNotReady() {
143
+ if(!this.isReady)
144
+ throw new Error('Database not ready !!! you need to `.init()` it')
145
+ }
146
+
147
+ /**
148
+ *
149
+ * @description database name
150
+ */
151
+ get name () {
152
+ return this.config.db_name ?? 'main';
153
+ }
154
+
155
+ /**
156
+ *
157
+ * @description Get the `storecraft` app
158
+ */
159
+ get app() {
160
+ return this.#_app;
161
+ }
162
+
163
+ /**
164
+ *
165
+ * @description Get the native `mongodb` client
166
+ */
167
+ get mongo_client() {
168
+ return this.#_mongo_client;
169
+ }
170
+
171
+ /**
172
+ *
173
+ * @description Get the config object
174
+ */
175
+ get config() {
176
+ return this.#_config;
177
+ }
178
+
179
+ /**
180
+ *
181
+ * @template {import('@storecraft/core/v-api').BaseType} T
182
+ *
183
+ *
184
+ * @param {string} name
185
+ *
186
+ *
187
+ * @returns {Collection<T>}
188
+ */
189
+ collection(name) {
190
+ return this.mongo_client.db(this.name).collection(name);
191
+ }
192
+
193
+ }
package/jsconfig.json ADDED
@@ -0,0 +1,14 @@
1
+ {
2
+ "compilerOptions": {
3
+ "checkJs": true,
4
+ "moduleResolution": "NodeNext",
5
+ "module": "NodeNext",
6
+ "composite": true,
7
+ },
8
+ "include": [
9
+ "*",
10
+ "src/*",
11
+ "tests/*.js",
12
+ "migrations/*",
13
+ ]
14
+ }
package/migrate.js ADDED
@@ -0,0 +1,39 @@
1
+ import { fileURLToPath } from "node:url";
2
+ import { MongoDB } from "./index.js";
3
+ import { config, up } from 'migrate-mongo';
4
+ import * as path from 'node:path';
5
+
6
+ const __filename = fileURLToPath(import.meta.url);
7
+ const __dirname = path.dirname(__filename);
8
+
9
+ /**
10
+ *
11
+ * @param {MongoDB} driver
12
+ * @param {boolean} [destroy_db_upon_completion=true]
13
+ */
14
+ export async function migrateToLatest(driver, destroy_db_upon_completion=true) {
15
+ console.log('Starting Migrations');
16
+
17
+ const config_migrate = {
18
+ migrationsDir: path.join(__dirname, 'migrations'),
19
+ changelogCollectionName: "migrations",
20
+ migrationFileExtension: ".js"
21
+ };
22
+
23
+ config.set(config_migrate);
24
+
25
+ driver.mongo_client.__db_driver = driver;
26
+
27
+ const results = await up(
28
+ driver.mongo_client.db(driver.name),
29
+ driver.mongo_client
30
+ );
31
+
32
+ console.log('results: \n', results)
33
+
34
+ if(destroy_db_upon_completion) {
35
+ console.log('disconnecting from mongo');
36
+ const success = await driver.disconnect();
37
+ console.log(`- success: ${success}`);
38
+ }
39
+ }
@@ -0,0 +1,60 @@
1
+ import { Db, MongoClient } from 'mongodb';
2
+
3
+ const collections = [
4
+ 'auth_users', 'collections', 'customers', 'discounts',
5
+ 'images', 'notifications', 'orders', 'posts',
6
+ 'products', 'shipping_methods', 'storefronts',
7
+ 'tags', 'templates'
8
+ ];
9
+
10
+ /**
11
+ *
12
+ * @param {Db} db
13
+ * @param {MongoClient} client
14
+ */
15
+ export async function up(db, client) {
16
+
17
+ const session = client.startSession();
18
+ try {
19
+ await session.withTransaction(async () => {
20
+ for (const collection_name of collections) {
21
+
22
+ await db.collection(collection_name).dropIndexes({ session });
23
+
24
+ await db.collection(collection_name).createIndexes(
25
+ [
26
+ {
27
+ key: { handle: 1 }, name: 'handle+',
28
+ background: false, unique: true, sparse: true
29
+ },
30
+ {
31
+ key: { updated_at: 1, _id: 1 }, name: '(updated_at+, _id+)',
32
+ background: false, sparse: true
33
+ },
34
+ {
35
+ key: { updated_at: -1, _id: -1 }, name: '(updated_at-, _id-)',
36
+ background: false, sparse: true
37
+ },
38
+ {
39
+ key: { search: 1 }, name: '(search+)',
40
+ background: false, sparse: true
41
+ },
42
+ ], {
43
+ session
44
+ }
45
+ )
46
+ }
47
+
48
+ });
49
+ } finally {
50
+ await session.endSession();
51
+ }
52
+ }
53
+
54
+ /**
55
+ *
56
+ * @param {Db} db
57
+ * @param {MongoClient} client
58
+ */
59
+ export async function down(db, client) {
60
+ }