@hackthedev/dsync-shop 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/.gitattributes ADDED
@@ -0,0 +1,2 @@
1
+ # Auto detect text files and perform LF normalization
2
+ * text=auto
@@ -0,0 +1,43 @@
1
+ name: Publish to npm
2
+
3
+ on:
4
+ push:
5
+ branches:
6
+ - main
7
+
8
+ permissions:
9
+ contents: write
10
+
11
+ jobs:
12
+ publish:
13
+ runs-on: ubuntu-latest
14
+
15
+ steps:
16
+ - uses: actions/checkout@v4
17
+ with:
18
+ persist-credentials: true
19
+
20
+ - name: Skip version bump commits
21
+ run: |
22
+ if git log -1 --pretty=%B | grep -q "chore: bump version"; then
23
+ echo "Version bump commit detected, skipping."
24
+ exit 0
25
+ fi
26
+
27
+ - uses: actions/setup-node@v4
28
+ with:
29
+ node-version: 20
30
+ registry-url: https://registry.npmjs.org/
31
+
32
+ - run: npm ci
33
+
34
+ - run: |
35
+ git config user.name "github-actions"
36
+ git config user.email "actions@github.com"
37
+ npm version patch -m "chore: bump version %s"
38
+ git push
39
+
40
+ - run: npm publish --access public
41
+ env:
42
+ NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
43
+
package/README.md ADDED
@@ -0,0 +1,296 @@
1
+ # dSyncShop
2
+
3
+ Part of the dSync library family. dSyncShop provides a complete shop system on top of dSyncPay, handling products, categories, orders and automatic post-purchase actions. It creates and manages its own database tables and registers all routes automatically.
4
+
5
+ ------
6
+
7
+ ## Setup
8
+
9
+ ```js
10
+ import dSyncShop from '@hackthedev/dsync-shop';
11
+ import dSyncSql from "@hackthedev/dsync-sql"
12
+ import dSyncPay from '@hackthedev/dsync-pay';
13
+
14
+ import express from "express";
15
+
16
+ const app = express();
17
+ const payments = new dSyncPay({...})
18
+ const db = new dSyncSql({...})
19
+
20
+ const shop = new dSyncShop({
21
+ app,
22
+ express,
23
+ payments, // dSyncPay instance
24
+ db, // dsync-sql instance
25
+ basePath: '/shop', // optional, default: '/shop'
26
+
27
+ isAdmin: async (req) => {
28
+ // return true or false
29
+ return req.headers['x-api-key'] === 'your-secret';
30
+ },
31
+
32
+ enrichMetadata: async (req) => {
33
+ // return an object that gets merged into payment metadata
34
+ // return null to reject the request (401)
35
+ const userId = req.headers['x-user-id'];
36
+ const token = req.headers['x-token'];
37
+ if (!userId || !token) return null;
38
+ return { userId, token };
39
+ },
40
+
41
+ productActions: {
42
+ 'give_role': {
43
+ label: 'Give Role',
44
+ params: [
45
+ { key: 'role', label: 'Role ID', type: 'text' }
46
+ ],
47
+ handler: async (metadata, product, params) => {
48
+ // metadata contains everything from enrichMetadata + product_id
49
+ // params contains the action_params set on the product
50
+ await giveRole(metadata.userId, params.role);
51
+ }
52
+ }
53
+ }
54
+ });
55
+ ```
56
+
57
+ ------
58
+
59
+ ## Options
60
+
61
+ | option | type | required | description |
62
+ | -------------- | -------------- | -------- | ----------------------------------------------------- |
63
+ | app | object | yes | express app instance |
64
+ | express | object | yes | express module |
65
+ | payments | object | yes | dSyncPay instance |
66
+ | db | object | yes | dsync-sql instance |
67
+ | basePath | string | no | route prefix, default: '/shop' |
68
+ | isAdmin | async function | yes | called before admin routes, return true/false |
69
+ | enrichMetadata | async function | no | called before payment creation, return object or null |
70
+ | productActions | object | no | action definitions, see product actions section |
71
+
72
+ ------
73
+
74
+ ## enrichMetadata
75
+
76
+ Called on every `POST /shop/payment/create` request. Use it to attach user data to the payment metadata server-side. The returned object gets merged into the metadata that dSyncPay carries through the payment flow and delivers to your callbacks.
77
+
78
+ Return `null` to abort the request with a 401.
79
+
80
+ ```js
81
+ enrichMetadata: async (req) => {
82
+ const userId = req.headers['x-user-id'];
83
+ const token = req.headers['x-token'];
84
+ if (!userId || !token) return null;
85
+
86
+ const valid = await verifyToken(userId, token);
87
+ if (!valid) return null;
88
+
89
+ return { userId, token };
90
+ }
91
+ ```
92
+
93
+ The client sends the headers:
94
+
95
+ ```js
96
+ fetch('/shop/payment/create', {
97
+ method: 'POST',
98
+ headers: {
99
+ 'Content-Type': 'application/json',
100
+ 'x-user-id': '12345',
101
+ 'x-token': 'your-token'
102
+ },
103
+ body: JSON.stringify({ product_id: 1, payment_method: 'paypal' })
104
+ });
105
+ ```
106
+
107
+ ------
108
+
109
+ ## Product Actions
110
+
111
+ Product actions let you define reusable logic that runs automatically after a successful purchase. Each action has a label, a list of params and a handler function.
112
+
113
+ ```js
114
+ productActions: {
115
+ 'give_role': {
116
+ label: 'Give Role',
117
+ params: [
118
+ { key: 'role', label: 'Role ID', type: 'text' }
119
+ ],
120
+ handler: async (metadata, product, params) => {
121
+ await giveRole(metadata.userId, params.role);
122
+ }
123
+ },
124
+ 'remove_role': {
125
+ label: 'Remove Role',
126
+ params: [
127
+ { key: 'role', label: 'Role ID', type: 'text' }
128
+ ],
129
+ handler: async (metadata, product, params) => {
130
+ await removeRole(metadata.userId, params.role);
131
+ }
132
+ },
133
+ 'give_coins': {
134
+ label: 'Give Coins',
135
+ params: [
136
+ { key: 'amount', label: 'Amount', type: 'number' }
137
+ ],
138
+ handler: async (metadata, product, params) => {
139
+ await addCoins(metadata.userId, params.amount);
140
+ }
141
+ }
142
+ }
143
+ ```
144
+
145
+ ### Param types
146
+
147
+ | type | description |
148
+ | ------ | ------------- |
149
+ | text | text input |
150
+ | number | numeric input |
151
+
152
+ ### Shorthand
153
+
154
+ If an action needs no params, you can pass a plain function instead of an object:
155
+
156
+ ```js
157
+ productActions: {
158
+ 'do_something': async (metadata, product, params) => {
159
+ // ...
160
+ }
161
+ }
162
+ ```
163
+
164
+ ### Setting action params on a product
165
+
166
+ When creating or updating a product you set which action it uses and what params to pass:
167
+
168
+ ```js
169
+ POST /shop/product/create
170
+ {
171
+ "name": "Premium Role",
172
+ "price": 9.99,
173
+ "action": "give_role",
174
+ "action_params": { "role": "123456789" }
175
+ }
176
+ ```
177
+
178
+ ------
179
+
180
+ ## Routes
181
+
182
+ ### Products
183
+
184
+ | method | route | auth | description |
185
+ | ------ | ----------------------------- | ------ | ------------------------------ |
186
+ | GET | /shop/products/list | public | list all active products |
187
+ | GET | /shop/products/list/:category | public | list products by category name |
188
+ | GET | /shop/product/:id | public | get single product |
189
+ | POST | /shop/product/create | admin | create product |
190
+ | POST | /shop/product/update/:id | admin | update product |
191
+ | DELETE | /shop/product/delete/:id | admin | delete product |
192
+
193
+ ### Categories
194
+
195
+ | method | route | auth | description |
196
+ | ------ | ------------------------- | ------ | ------------------- |
197
+ | GET | /shop/categories/list | public | list all categories |
198
+ | POST | /shop/category/create | admin | create category |
199
+ | POST | /shop/category/update/:id | admin | update category |
200
+ | DELETE | /shop/category/delete/:id | admin | delete category |
201
+
202
+ ### Actions
203
+
204
+ | method | route | auth | description |
205
+ | ------ | ------------------ | ----- | --------------------------------------- |
206
+ | GET | /shop/actions/list | admin | list all registered actions with params |
207
+
208
+ ### Payments
209
+
210
+ | method | route | auth | description |
211
+ | ------ | -------------------- | -------------- | ------------------------------------------------- |
212
+ | POST | /shop/payment/create | enrichMetadata | create a paypal or coinbase payment for a product |
213
+
214
+ ------
215
+
216
+ ## Product Create / Update Body
217
+
218
+ ```js
219
+ {
220
+ name: 'Premium Role', // required
221
+ price: 9.99, // required
222
+ description: '...', // optional
223
+ category_id: 1, // optional
224
+ image_url: 'https://...', // optional
225
+ stock: 0, // optional, default: 0
226
+ active: 1, // optional, default: 1
227
+ action: 'give_role', // optional, must match a registered productAction key
228
+ action_params: { role: '...' } // optional, object, stored as json
229
+ }
230
+ ```
231
+
232
+ ------
233
+
234
+ ## Payment Create Body
235
+
236
+ ```js
237
+ POST /shop/payment/create
238
+
239
+ {
240
+ product_id: 1,
241
+ payment_method: 'paypal' // or 'crypto'
242
+ }
243
+ ```
244
+
245
+ Response for paypal:
246
+
247
+ ```js
248
+ {
249
+ error: null,
250
+ approvalUrl: 'https://paypal.com/...',
251
+ orderId: '...'
252
+ }
253
+ ```
254
+
255
+ Response for crypto:
256
+
257
+ ```js
258
+ {
259
+ error: null,
260
+ hostedUrl: 'https://commerce.coinbase.com/...',
261
+ chargeCode: '...'
262
+ }
263
+ ```
264
+
265
+ ------
266
+
267
+ ## Database Tables
268
+
269
+ dSyncShop automatically creates and manages the following tables via `checkAndCreateTable`:
270
+
271
+ | table | description |
272
+ | ----------- | --------------------------------------------- |
273
+ | categories | product categories with optional parent |
274
+ | products | products with action and action_params fields |
275
+ | orders | completed, failed and cancelled orders |
276
+ | order_items | line items per order |
277
+
278
+ ------
279
+
280
+ ## isAdmin
281
+
282
+ Called before every admin route. If not provided, all admin routes are public. Return `true` to allow, `false` to respond with 403.
283
+
284
+ ```js
285
+ isAdmin: async (req) => {
286
+ return req.headers['x-api-key'] === 'your-secret'; // or whatever
287
+ }
288
+ ```
289
+
290
+ You can also use it as a standalone middleware in your own routes:
291
+
292
+ ```js
293
+ app.get('/something', shop.adminMiddleware(), (req, res) => {
294
+ res.json({ ok: true });
295
+ });
296
+ ```