@storecraft/sdk 0.1.0

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/src/bots.js ADDED
@@ -0,0 +1,113 @@
1
+ import { StorecraftSDK } from '../index.js'
2
+
3
+ export const SECOND = 1000
4
+ export const MINUTE = SECOND*60
5
+
6
+
7
+ /**
8
+ * @typedef {object} BotMetaData
9
+ * @property {number} updatedAt
10
+ *
11
+ */
12
+
13
+ const NAME = 'bots'
14
+
15
+ export default class Bots {
16
+
17
+ /**
18
+ * @param {StorecraftSDK} context
19
+ */
20
+ constructor(context) {
21
+ this.context = context
22
+ this.db = context.db
23
+ }
24
+
25
+ init = async () => {
26
+ // this.activitySpy()
27
+ this.activityBot()
28
+ setInterval(
29
+ this.activityBot,
30
+ MINUTE*10
31
+ )
32
+ }
33
+
34
+ activityBot = async () => {
35
+ // console.trace()
36
+ try {
37
+ const db = this.db.firebaseDB
38
+ const result = await runTransaction(
39
+ this.db.firebaseDB,
40
+ async t => {
41
+ const now = Date.now()
42
+ const ref = doc(db, NAME, 'shelf-activity-bot')
43
+ const m = await t.get(ref)
44
+ const updatedAt = m.data()?.updatedAt ?? 0
45
+
46
+ const q = {
47
+ where: [
48
+ ['updatedAt', '>', updatedAt]
49
+ ]
50
+ }
51
+
52
+ const cols = ['collections', 'products', 'users', 'tags', 'discounts']
53
+ const results = cols.map(
54
+ async c => [c, await this.db.col(c).count(q)]
55
+ )
56
+
57
+ t.set(
58
+ m.ref,
59
+ {
60
+ updatedAt: now
61
+ }
62
+ )
63
+
64
+ return await Promise.all(results)
65
+
66
+ },
67
+ {
68
+ maxAttempts: 1
69
+ }
70
+ )
71
+
72
+ let filtered = result.filter(
73
+ it => it[1]
74
+ )
75
+
76
+ if(filtered.length==0)
77
+ return
78
+
79
+ const search = filtered.map(it => it[0])
80
+ const messages = filtered.map(
81
+ it => {
82
+ const [c, count] = it
83
+ return `\n* 🚀 **${count}** \`${c}\` were updated`
84
+ }
85
+ );
86
+
87
+ const message = ["**Latest Activity updates**", ...messages].join(' ')
88
+ /**@type {NotificationData} */
89
+ const noti = {
90
+ message,
91
+ search,
92
+ author: 'shelf-activity-bot 🤖',
93
+ updatedAt: Date.now()
94
+ }
95
+
96
+ // console.log(messages)
97
+ // console.log(message)
98
+
99
+ await this.context.notifications.add(
100
+ noti
101
+ )
102
+
103
+ } catch (e) {
104
+ // failed because another the document updated from another client
105
+ // this is ok
106
+ console.log(e)
107
+ }
108
+
109
+
110
+
111
+ }
112
+
113
+ }
@@ -0,0 +1,129 @@
1
+ import { StorecraftSDK } from '../index.js'
2
+ import { collection_base } from './utils.api.fetch.js';
3
+ import { filter_fields, filter_unused } from './utils.functional.js';
4
+
5
+ /**
6
+ * Base `collections` **CRUD**
7
+ *
8
+ * @extends {collection_base<
9
+ * import('@storecraft/core/v-api').CollectionTypeUpsert,
10
+ * import('@storecraft/core/v-api').CollectionType>
11
+ * }
12
+ */
13
+ export default class Collections extends collection_base {
14
+
15
+ /**
16
+ *
17
+ * @param {StorecraftSDK} sdk
18
+ */
19
+ constructor(sdk) {
20
+ super(sdk, 'collections');
21
+ }
22
+
23
+ /**
24
+ *
25
+ * @param {string} collection_handle
26
+ * @param {number} limit
27
+ */
28
+ publish = async (collection_handle, limit=1000) => {
29
+ throw new Error('Implement me !!!')
30
+ // extra filtering for validation
31
+
32
+ try {
33
+ // fetch collection
34
+ var [exists, id, collection] = await this.get(collection_handle)
35
+ } catch (e) {
36
+ throw 'Collection read error: ' + String(e)
37
+ }
38
+
39
+ // get all products in collection
40
+ try {
41
+ var pick_data = items => items.map(item => item[1])
42
+ /** @param {ProductData[]} items */
43
+ var filter_hard_on_collection = items => items.filter(
44
+ item => (
45
+ item.collections &&
46
+ item.collections.indexOf(collection_handle)>=0 &&
47
+ item.qty>0 &&
48
+ (item.active || item.active===undefined) &&
49
+ (!item.parent_handle)
50
+ )
51
+ )
52
+ const filter_fields_in = filter_fields(
53
+ 'title', 'handle', 'media', 'desc', 'price', 'attributes',
54
+ 'video', 'tags', 'updatedAt', 'compareAtPrice', 'discounts',
55
+ 'parent_handle', 'variants_options', 'variants_products'
56
+ )
57
+ var products = await this.context.products.list(
58
+ [`col:${collection_handle}`], limit
59
+ )
60
+ // console.log('products', products)
61
+ products = pick_data(products)
62
+ products = filter_hard_on_collection(products)
63
+ products = filter_fields_in(products)
64
+ products = filter_unused(products)
65
+ } catch (e) {
66
+ throw 'products read error: ' + String(e)
67
+ }
68
+
69
+ try {
70
+ // upload collection export
71
+ const metadata = {
72
+ contentType: 'application/json',
73
+ contentEncoding: 'gzip',
74
+ cacheControl: `max-age=${60*60*1}, must-revalidate`
75
+ // cacheControl: `private, max-age=${60*60*1}`
76
+ // cacheControl: `max-age=${60*60*5}, must-revalidate`
77
+ }
78
+ var [url, ref] = await this.context.storage.uploadBytes(
79
+ `collections/${collection_handle}.json`,
80
+ pako.gzip(
81
+ JSON.stringify({
82
+ ...collection,
83
+ products
84
+ })
85
+ ),
86
+ metadata
87
+ )
88
+ } catch(e) {
89
+ throw 'Collection upload error: ' + String(e)
90
+ }
91
+
92
+ try {
93
+ await this.update(collection_handle, { _published: url })
94
+ } catch (e) {
95
+ throw 'Collection update error: ' + String(e)
96
+ }
97
+ }
98
+
99
+ // /**
100
+ // * Add tags in bulk to products in collection
101
+ // * @param {string} colId
102
+ // * @param {string[]} tags
103
+ // * @param {boolean} add true for add false for remove
104
+ // */
105
+ // bulkAddRemoveTags = async (colId, tags, add=true) => {
106
+
107
+ // // first get all products in collection
108
+ // const tag_all = tags ?? []
109
+ // const tag_all_prefixed = tag_all.map(t => `tag:${t}`)
110
+ // const tag_vs = tag_all.map(it => it.split('_').pop())
111
+
112
+ // var products = await this.context.products.list([`col:${colId}`], 10000)
113
+ // // console.log('products ', products)
114
+ // // console.log('colId ', colId)
115
+ // const batch = writeBatch(this.context.firebase.db)
116
+ // products.forEach(it => {
117
+ // const ref = doc(this.context.firebase.db, 'products', it[0])
118
+ // batch.update(ref, {
119
+ // tags : add ? arrayUnion(...tags) : arrayRemove(...tags),
120
+ // search : add ? arrayUnion(...tag_all, ...tag_all_prefixed, ...tag_vs) :
121
+ // arrayRemove(...tag_all, ...tag_all_prefixed, ...tag_vs),
122
+ // updatedAt : Date.now()
123
+ // })
124
+ // })
125
+ // await batch.commit()
126
+
127
+ // }
128
+
129
+ }
@@ -0,0 +1,21 @@
1
+ import { StorecraftSDK } from '../index.js'
2
+ import { collection_base } from './utils.api.fetch.js';
3
+
4
+ /**
5
+ * Base `customers` **CRUD**
6
+ *
7
+ * @extends {collection_base<
8
+ * import('@storecraft/core/v-api').CustomerTypeUpsert,
9
+ * import('@storecraft/core/v-api').CustomerType>
10
+ * }
11
+ */
12
+ export default class Customers extends collection_base {
13
+
14
+ /**
15
+ *
16
+ * @param {StorecraftSDK} sdk
17
+ */
18
+ constructor(sdk) {
19
+ super(sdk, 'customers');
20
+ }
21
+ }
@@ -0,0 +1,149 @@
1
+ import { StorecraftSDK } from '../index.js'
2
+ import { collection_base } from './utils.api.fetch.js';
3
+
4
+ /**
5
+ * Base `discounts` **CRUD**
6
+ *
7
+ * @extends {collection_base<
8
+ * import('@storecraft/core/v-api').DiscountTypeUpsert,
9
+ * import('@storecraft/core/v-api').DiscountType>
10
+ * }
11
+ */
12
+ export default class Discounts extends collection_base {
13
+
14
+ /**
15
+ *
16
+ * @param {StorecraftSDK} sdk
17
+ */
18
+ constructor(sdk) {
19
+ super(sdk, 'discounts');
20
+ }
21
+
22
+ /**
23
+ *
24
+ * @param {DiscountData} discount_data
25
+ * @param {number} limit
26
+ */
27
+ publish = async (discount_data, limit=10000) => {
28
+ const coll_handle = `discount-${discount_data.code}`
29
+ const dd = {
30
+ ...discount_data,
31
+ _published: coll_handle
32
+ }
33
+ if(dd.info.details.meta.type==='order')
34
+ throw 'Exporting a discount collection is only available for \
35
+ Product discounts (you chose Order discount)'
36
+
37
+ // save current document, allow to fail
38
+ await this.set(
39
+ dd.code, dd
40
+ )
41
+ // await this.update(dd.code, { _published: coll_handle })
42
+
43
+
44
+ // first, remove all previous collection tag from previous products
45
+ try {
46
+ const products_to_remove =
47
+ await this.context.products.list([`col:${coll_handle}`], 10000)
48
+ // console.log('products_to_remove ', products_to_remove);
49
+ const batch_remove = writeBatch(this.context.firebase.db)
50
+ products_to_remove.forEach(it => {
51
+ const ref = doc(this.context.firebase.db, 'products', it[0])
52
+ batch_remove.update(ref, {
53
+ search : arrayRemove(`col:${coll_handle}`),
54
+ collections : arrayRemove(coll_handle),
55
+ [`discounts.${dd.code}`]: deleteField()
56
+ })
57
+ })
58
+ await batch_remove.commit()
59
+
60
+ } catch (e) {
61
+ console.log('Remove old: ' + String(e))
62
+ console.log(e)
63
+ }
64
+
65
+ try {
66
+ // filter in product filters
67
+ var product_filters = dd.info.filters.filter(f => f.meta.type==='product')
68
+
69
+ // then, make a server search that will filter out as much as possible
70
+ /**@type {Filter} */
71
+ var first_guided_filter = undefined
72
+ /**@type {string[]} */
73
+ var first_guided_search_terms = undefined
74
+
75
+ if(first_guided_filter = product_filters.find(f => f.meta.op==='p-in-handles')) {
76
+ first_guided_search_terms = first_guided_filter.value
77
+ }
78
+ else if(first_guided_filter = product_filters.find(f => f.meta.op==='p-in-tags')) {
79
+ first_guided_search_terms = first_guided_filter.value.map(t => `tag:${t}`)
80
+ }
81
+ else if(first_guided_filter = product_filters.find(f => f.meta.op==='p-in-collections')) {
82
+ first_guided_search_terms = first_guided_filter.value.map(c => `col:${c}`)
83
+ }
84
+ } catch (e) {
85
+ throw 'Filter preparing error: ' + String(e)
86
+ }
87
+
88
+ try {
89
+ // then, global filtering, this helps to reduce legal products for filtering
90
+ var products = await
91
+ this.context.products.list(first_guided_search_terms, limit)
92
+
93
+ // now local filtering (due to firebase limitations with filtering)
94
+ var filtered_products =
95
+ this.filterProductsWithFilters(products, product_filters)
96
+
97
+ // products = products.slice(0, 400)
98
+ } catch (e) {
99
+ throw 'Filtering error: ' + String(e)
100
+ }
101
+
102
+ try {
103
+ // add collection tag to each product with batch write
104
+ const batch = writeBatch(this.context.firebase.db)
105
+ filtered_products.forEach(it => {
106
+ const p = it[1]
107
+ const isActive = p?.active==true || (p.active===undefined)
108
+ if(!isActive)
109
+ return;
110
+
111
+ const ref = doc(this.context.firebase.db, 'products', it[0])
112
+ const dd_mod = {...dd}
113
+ delete dd_mod.search
114
+ delete dd_mod.order
115
+ batch.update(ref, {
116
+ collections : arrayUnion(coll_handle),
117
+ search : arrayUnion(`col:${coll_handle}`),
118
+ [`discounts.${dd_mod.code}`]: dd_mod
119
+ })
120
+ })
121
+ await batch.commit()
122
+ } catch (e) {
123
+ throw 'Products update failed: ' + String(e)
124
+ }
125
+
126
+ try {
127
+ // now, create a new collection
128
+ /**@type {import('./js-docs-types').CollectionData} */
129
+ const col_discount = {
130
+ desc : dd.desc,
131
+ handle : coll_handle,
132
+ title : dd.title,
133
+ media : dd.media,
134
+ tags : dd.tags,
135
+ attributes: dd.attributes,
136
+ createdAt: Date.now()
137
+ }
138
+
139
+ await this.context.collections.set(
140
+ col_discount.handle, col_discount
141
+ )
142
+ // await this.update(discount_data.code, { _published: coll_handle })
143
+ } catch (e) {
144
+ throw 'Collection creation failed: ' + String(e)
145
+ }
146
+
147
+ }
148
+
149
+ }
package/src/images.js ADDED
@@ -0,0 +1,21 @@
1
+ import { StorecraftSDK } from '../index.js'
2
+ import { collection_base } from './utils.api.fetch.js';
3
+
4
+ /**
5
+ * Base `images` **CRUD**
6
+ *
7
+ * @extends {collection_base<
8
+ * import('@storecraft/core/v-api').ImageTypeUpsert,
9
+ * import('@storecraft/core/v-api').ImageType>
10
+ * }
11
+ */
12
+ export default class Images extends collection_base {
13
+
14
+ /**
15
+ *
16
+ * @param {StorecraftSDK} sdk
17
+ */
18
+ constructor(sdk) {
19
+ super(sdk, 'images');
20
+ }
21
+ }
@@ -0,0 +1,102 @@
1
+ import { StorecraftSDK } from '../index.js'
2
+ import {
3
+ collection_base, fetchApiWithAuth
4
+ } from './utils.api.fetch.js';
5
+
6
+ /**
7
+ * Base `notifications` **CRUD**
8
+ *
9
+ * @extends {collection_base<
10
+ * import('@storecraft/core/v-api').NotificationTypeUpsert,
11
+ * import('@storecraft/core/v-api').NotificationType>
12
+ * }
13
+ */
14
+ export default class Notifications extends collection_base {
15
+
16
+ /**
17
+ *
18
+ * @param {StorecraftSDK} sdk
19
+ */
20
+ constructor(sdk) {
21
+ super(sdk, 'notifications');
22
+ }
23
+
24
+ /**
25
+ *
26
+ * @param {import('@storecraft/core/v-api').NotificationTypeUpsert[]} items
27
+ */
28
+ upsertBulk = items => {
29
+ return fetchApiWithAuth(
30
+ this.sdk,
31
+ `${this.base_name}`,
32
+ {
33
+ method: 'post',
34
+ body: JSON.stringify(items),
35
+ headers: {
36
+ 'Content-Type': 'application/json'
37
+ }
38
+ }
39
+ );
40
+ }
41
+
42
+ meta = () => {
43
+ return this.get('_meta')
44
+ }
45
+
46
+ /**
47
+ * Test if backend moght have new data economically
48
+ * @returns {Promise<boolean>}
49
+ */
50
+ hasChanged = async () => {
51
+ try {
52
+ // try cache
53
+ const cached = await this.list([], 50, true, false)
54
+ if (cached.length==0)
55
+ return true
56
+
57
+ // compute how many latest updates with max timestamp in cache
58
+ const max_updated = cached.reduce(
59
+ (p, c) => {
60
+ const updatedAt = c[1]?.updatedAt
61
+
62
+ if(updatedAt==p.timestamp)
63
+ p.count+=1
64
+ else if(updatedAt>p.timestamp)
65
+ p.count=1
66
+
67
+ p.timestamp = Math.max(updatedAt ?? -1, p.timestamp)
68
+ return p
69
+ },
70
+ {
71
+ timestamp: 0,
72
+ count: 0
73
+ }
74
+ )
75
+
76
+ // now, use a light count query to the database
77
+ const count = await this.context.db.col(NAME).count(
78
+ {
79
+ where: [
80
+ ['updatedAt', '>=', max_updated.timestamp]
81
+ ]
82
+ }
83
+ )
84
+
85
+ // console.log(cached)
86
+ // console.log('count ', count)
87
+ // console.log('max_updated ', max_updated)
88
+
89
+ if(count > max_updated.count)
90
+ return true
91
+
92
+ return false
93
+
94
+ } catch (e) {
95
+ // error
96
+ console.error(e)
97
+ return true
98
+ }
99
+
100
+ }
101
+
102
+ }
package/src/orders.js ADDED
@@ -0,0 +1,47 @@
1
+ import { calculate_pricing } from '@storecraft/core/v-api/con.pricing.logic.js';
2
+ import { StorecraftSDK } from '../index.js'
3
+ import { collection_base } from './utils.api.fetch.js';
4
+
5
+ /**
6
+ * Base `orders` **CRUD**
7
+ *
8
+ * @extends {collection_base<
9
+ * import('@storecraft/core/v-api').OrderDataUpsert,
10
+ * import('@storecraft/core/v-api').OrderData>
11
+ * }
12
+ */
13
+ export default class Orders extends collection_base {
14
+
15
+ /**
16
+ *
17
+ * @param {StorecraftSDK} sdk
18
+ */
19
+ constructor(sdk) {
20
+ super(sdk, 'orders');
21
+ }
22
+
23
+ /**
24
+ * calculate pricing of line items
25
+ *
26
+ * @param {import('@storecraft/core/v-api').LineItem[]} line_items
27
+ * @param {import('@storecraft/core/v-api').DiscountType[]} coupons
28
+ * @param {import('@storecraft/core/v-api').ShippingMethodType} shipping_method
29
+ * @param {string} [uid]
30
+ */
31
+ calculatePricing = async (
32
+ line_items, coupons=[], shipping_method, uid
33
+ ) => {
34
+ // fetch auto discounts
35
+ const auto_discounts = await this.sdk.discounts.list(
36
+ {
37
+ limit: 100,
38
+ vql: 'app:automatic'
39
+ }
40
+ );
41
+
42
+ return calculate_pricing(
43
+ line_items, auto_discounts, coupons, shipping_method, uid
44
+ );
45
+ }
46
+
47
+ }
@@ -0,0 +1,89 @@
1
+ import { StorecraftSDK } from '../index.js'
2
+ import {
3
+ fetchApiWithAuth, get, list
4
+ } from './utils.api.fetch.js';
5
+
6
+ /**
7
+ *
8
+ */
9
+ export default class Payments {
10
+
11
+ /** @type {import('../index.js').StorecraftSDK} */
12
+ #sdk = undefined;
13
+
14
+ /**
15
+ *
16
+ * @param {StorecraftSDK} sdk
17
+ */
18
+ constructor(sdk) {
19
+ this.#sdk = sdk;
20
+ }
21
+
22
+ /**
23
+ *
24
+ * @param {string} handle payment gateway `handle`
25
+ *
26
+ *
27
+ * @returns {Promise<import('@storecraft/core/v-api').PaymentGatewayItemGet>}
28
+ */
29
+ get(handle) {
30
+ return get(this.sdk, 'payments/gateways', handle);
31
+ }
32
+
33
+ /**
34
+ *
35
+ *
36
+ * @returns {Promise<import('@storecraft/core/v-api').PaymentGatewayItemGet[]>}
37
+ */
38
+ list() {
39
+ return list(this.sdk, 'payments/gateways');
40
+ }
41
+
42
+
43
+ /**
44
+ *
45
+ * Consult with the `payment` gateway about the payment
46
+ * status of an `order`.
47
+ *
48
+ *
49
+ * @param {string} order_id
50
+ *
51
+ * @returns {Promise<import('@storecraft/core/v-api').PaymentGatewayStatus>}
52
+ */
53
+ paymentStatusOfOrder(order_id) {
54
+ return fetchApiWithAuth(
55
+ this.sdk,
56
+ `/payments/status/${order_id}`,
57
+ {
58
+ method: 'get'
59
+ }
60
+ )
61
+ }
62
+
63
+ /**
64
+ *
65
+ * Invoke a `payment gateway` action on `order`
66
+ *
67
+ *
68
+ * @param {string} action_handle The `action` handle at the gateway
69
+ * @param {string} order_id the `id` of the `order`
70
+ *
71
+ *
72
+ * @returns {Promise<import('@storecraft/core/v-api').PaymentGatewayStatus>}
73
+ */
74
+ invokeAction(action_handle, order_id) {
75
+ return fetchApiWithAuth(
76
+ this.sdk,
77
+ `/payments/${action_handle}/${order_id}`,
78
+ {
79
+ method: 'post'
80
+ }
81
+ )
82
+ }
83
+
84
+
85
+ get sdk() {
86
+ return this.#sdk;
87
+ }
88
+
89
+ }
package/src/posts.js ADDED
@@ -0,0 +1,22 @@
1
+ import { StorecraftSDK } from '../index.js'
2
+ import { collection_base } from './utils.api.fetch.js';
3
+
4
+ /**
5
+ * Base `posts` **CRUD**
6
+ *
7
+ * @extends {collection_base<
8
+ * import('@storecraft/core/v-api').PostTypeUpsert,
9
+ * import('@storecraft/core/v-api').PostType>
10
+ * }
11
+ */
12
+ export default class Posts extends collection_base {
13
+
14
+ /**
15
+ *
16
+ * @param {StorecraftSDK} sdk
17
+ */
18
+ constructor(sdk) {
19
+ super(sdk, 'posts');
20
+ }
21
+
22
+ }