webcake-storefront-mcp 1.31.0 → 1.31.2
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/dist/changelog.json +14 -14
- package/dist/guides.js +115 -45
- package/package.json +1 -1
package/dist/changelog.json
CHANGED
|
@@ -1,4 +1,18 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"v": "1.31.2",
|
|
4
|
+
"d": "26/06/2026",
|
|
5
|
+
"type": "Changed",
|
|
6
|
+
"en": "The HTTP_FUNCTION_GUIDE embedded in get_http_function and get_site_custom_code now includes a \"Common patterns\" section with battle-tested…",
|
|
7
|
+
"vi": "HTTP_FUNCTION_GUIDE được nhúng trong get_http_function và get_site_custom_code nay bổ sung phần \"Common patterns\" với các recipe đã được kiểm chứng…"
|
|
8
|
+
},
|
|
9
|
+
{
|
|
10
|
+
"v": "1.31.1",
|
|
11
|
+
"d": "26/06/2026",
|
|
12
|
+
"type": "Fixed",
|
|
13
|
+
"en": "The webcake-data SDK reference embedded in get_http_function and get_site_custom_code now documents the correct Mongoose-document API: filters are…",
|
|
14
|
+
"vi": "Tài liệu tham chiếu SDK webcake-data được nhúng trong get_http_function và get_site_custom_code nay ghi lại đúng API kiểu Mongoose-document: bộ lọc…"
|
|
15
|
+
},
|
|
2
16
|
{
|
|
3
17
|
"v": "1.31.0",
|
|
4
18
|
"d": "26/06/2026",
|
|
@@ -26,19 +40,5 @@
|
|
|
26
40
|
"type": "Added",
|
|
27
41
|
"en": "New scaffold_popup tool builds and saves a designed newsletter/promo popup (heading + subtext + email subscribe form + close button, centred modal)…",
|
|
28
42
|
"vi": "Tool mới scaffold_popup xây dựng và lưu một popup newsletter/promo được thiết kế sẵn (tiêu đề + phụ đề + form đăng ký email + nút đóng, dạng modal…"
|
|
29
|
-
},
|
|
30
|
-
{
|
|
31
|
-
"v": "1.27.0",
|
|
32
|
-
"d": "26/06/2026",
|
|
33
|
-
"type": "Added",
|
|
34
|
-
"en": "build_page now accepts an seo object (title, description, keyword, favicon, thumbnail) that is written to page.settings.seo with Open Graph…",
|
|
35
|
-
"vi": "build_page nay nhận thêm đối tượng seo (title, description, keyword, favicon, thumbnail) và ghi vào page.settings.seo kèm mirroring Open Graph; hỗ…"
|
|
36
|
-
},
|
|
37
|
-
{
|
|
38
|
-
"v": "1.26.1",
|
|
39
|
-
"d": "26/06/2026",
|
|
40
|
-
"type": "Fixed",
|
|
41
|
-
"en": "list_products now correctly unwraps the { products, total_product } API response shape, returning a product array and accurate total instead of…",
|
|
42
|
-
"vi": "list_products nay giải nén đúng kiểu phản hồi { products, total_product } từ API, trả về mảng sản phẩm và tổng chính xác thay vì trả thẳng đối tượng…"
|
|
43
43
|
}
|
|
44
44
|
]
|
package/dist/guides.js
CHANGED
|
@@ -22,40 +22,87 @@ ANY method → /api/v1/{site_id}/_functions/{FunctionName}
|
|
|
22
22
|
The caller receives your return value as data.result. From the storefront, the
|
|
23
23
|
webcake-fn client (api.method_FunctionName(params)) returns that result directly.
|
|
24
24
|
|
|
25
|
-
## webcake-data — Database SDK (built-in)
|
|
25
|
+
## webcake-data — Database SDK (built-in). This is the MAIN way to read/write a site's collections.
|
|
26
26
|
import { DBConnection } from 'webcake-data';
|
|
27
27
|
const db = new DBConnection(); // auto-uses the sandbox global site/token
|
|
28
|
-
const Model = db.model('collection_name');
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
- Model.
|
|
36
|
-
- Model.
|
|
37
|
-
- Model.
|
|
38
|
-
- Model.
|
|
39
|
-
|
|
40
|
-
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
.
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
### Populate (
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
28
|
+
const Model = db.model('collection_name'); // 'collection_name' = the table_name of a collection
|
|
29
|
+
|
|
30
|
+
The API is Mongoose-DOCUMENT style: filters are plain MongoDB-style OBJECTS, chains are
|
|
31
|
+
DIRECTLY AWAITABLE (NO .exec()), and you select fields with an ARRAY. Every document has an
|
|
32
|
+
\`id\` (UUID string) plus \`inserted_at\` / \`updated_at\`.
|
|
33
|
+
|
|
34
|
+
### Read
|
|
35
|
+
- await Model.findOne(filter) → one doc (or null)
|
|
36
|
+
- await Model.findOne(filter, { populate:{ field, select:[...] } })
|
|
37
|
+
- await Model.find(filter).select([...]).populate({...}).sort({...}).limit(n) → array (await the CHAIN; no .exec())
|
|
38
|
+
- await Model.countDocuments(filter) → number
|
|
39
|
+
|
|
40
|
+
filter is a MongoDB-style object. Operators go INSIDE the field value:
|
|
41
|
+
{ thanh_vien: userId } // equals
|
|
42
|
+
{ status: { $in: [0, 1, 2] } } // in a list
|
|
43
|
+
{ trang_thai_tg: { $ne: 3 } } // not equal
|
|
44
|
+
// also $nin, $gt, $gte, $lt, $lte, $exists — the usual MongoDB query operators.
|
|
45
|
+
|
|
46
|
+
Chain methods on a Model.find(filter):
|
|
47
|
+
.select(["id", "name", "diem_so"]) // ARRAY of field names to return
|
|
48
|
+
.sort({ inserted_at: -1 }) // 1 ascending, -1 descending (multi-key ok)
|
|
49
|
+
.limit(20) .skip(0)
|
|
50
|
+
.populate({ field: "thanh_vien", select: ["id", "name", "avatar"] })
|
|
51
|
+
|
|
52
|
+
### Populate (resolve a reference field)
|
|
53
|
+
A reference field stores the related row's \`id\` (a string). \`.populate({ field, select:[...] })\`
|
|
54
|
+
replaces it with the related OBJECT (only the selected fields). After populate, read it as an
|
|
55
|
+
object; before/without populate it's the raw id string. Handle both:
|
|
56
|
+
const id = typeof row.thanh_vien === "object" ? row.thanh_vien.id : row.thanh_vien;
|
|
57
|
+
populate also works as a 2nd-arg option on findOne: Model.findOne(filter, { populate:{ field, select:[...] } }).
|
|
58
|
+
|
|
59
|
+
### Write
|
|
60
|
+
- await Model.create(doc) → created doc (use doc.id afterwards)
|
|
61
|
+
- await Model.findOneAndUpdate(filter, update, { new: true }) → the UPDATED doc ({ new:true } = return the new version)
|
|
62
|
+
- await Model.updateOne(filter, update) → write result
|
|
63
|
+
- await Model.updateMany(filter, update) → bulk update matching rows
|
|
64
|
+
- await Model.deleteMany(filter) → delete matching rows
|
|
65
|
+
(update is a plain object of the fields to set, e.g. { status: 1, ty_le_thang: 75 }.)
|
|
66
|
+
|
|
67
|
+
### Real example pattern (from a production function)
|
|
68
|
+
const Members = db.model("thanh_vien_ps");
|
|
69
|
+
const rows = await Members
|
|
70
|
+
.find({ playspace: psId, status: { $in: [0, 1, 2] } })
|
|
71
|
+
.select(["id", "thanh_vien", "tien_con_lai", "status"])
|
|
72
|
+
.populate({ field: "thanh_vien", select: ["id", "name", "avatar"] })
|
|
73
|
+
.sort({ status: 1, inserted_at: 1 })
|
|
74
|
+
.limit(50);
|
|
75
|
+
const count = await Members.countDocuments({ playspace: psId, status: { $in: [0, 1, 2] } });
|
|
76
|
+
const created = await Members.create({ thanh_vien: userId, playspace: psId, status: 0 });
|
|
77
|
+
await Members.updateOne({ id: created.id }, { tien_con_lai: 100000 });
|
|
78
|
+
|
|
79
|
+
### Common patterns (battle-tested in production functions)
|
|
80
|
+
- FIND-OR-CREATE (ensure a row exists):
|
|
81
|
+
let row = await M.findOne({ key: v });
|
|
82
|
+
if (!row) row = await M.create({ key: v, ...defaults });
|
|
83
|
+
return row;
|
|
84
|
+
- UPSERT + read back the new version:
|
|
85
|
+
const updated = await M.findOneAndUpdate({ key: v }, { field: x }, { new: true });
|
|
86
|
+
- COUNT then DENORMALIZE onto a parent (cheap reads later):
|
|
87
|
+
const n = await Members.countDocuments({ playspace: psId, status: { $in: [0,1,2] } });
|
|
88
|
+
await PlaySpace.updateOne({ id: psId }, { so_thanh_vien: n });
|
|
89
|
+
- MULTI-TABLE write (no transactions — do the steps in order, validate first):
|
|
90
|
+
for (const it of items) await Members.updateOne({ id: it.id }, { tien_con_lai: it.bal });
|
|
91
|
+
await PlaySpace.updateOne({ id: psId }, { tien_con_lai: total });
|
|
92
|
+
await History.create({ playspace: psId, items }); // append an audit/history row
|
|
93
|
+
- SOFT DELETE (keep history) — set a status instead of deleteMany:
|
|
94
|
+
await M.updateOne({ id }, { status: INACTIVE }); // and filter it out with { status: { $ne: INACTIVE } }
|
|
95
|
+
- REFERENCE id helpers (a ref field is an id string, or an object after populate):
|
|
96
|
+
const toId = (v) => (v && typeof v === "object" ? String(v.id || "") : String(v || ""));
|
|
97
|
+
const toNumber = (v, d = 0) => (Number.isFinite(Number(v)) ? Number(v) : d);
|
|
98
|
+
- STATUS as numeric enums (define constants up top): const STATUS = { OWNER:0, ACTIVE:1, GUEST:2, INACTIVE:3 };
|
|
99
|
+
then query with { status: { $in: [STATUS.OWNER, STATUS.ACTIVE] } }.
|
|
100
|
+
- ALWAYS guard auth + wrap in try/catch returning a coded mess:
|
|
101
|
+
const userId = request.customer?.id ?? ""; if (!userId) return { mess: "NO_ACCOUNT_CALL" };
|
|
102
|
+
try { /* … */ return { mess: "OK", ...data }; } catch (err) { console.error(err?.message || err); return { mess: "SYSTEM_ERROR" }; }
|
|
103
|
+
|
|
104
|
+
## Built-in @webcake/* modules (first arg is always request; they auth via global.token — these run
|
|
105
|
+
## INSIDE the function sandbox, so they reach the /cms_function endpoints the dashboard JWT can't)
|
|
59
106
|
Thin wrappers over the backend's /cms_function/{site_id}/... endpoints. Pass request so
|
|
60
107
|
they pick up site_id. Below is EXACTLY what each call sends to the backend + what it returns.
|
|
61
108
|
|
|
@@ -75,15 +122,20 @@ they pick up site_id. Below is EXACTLY what each call sends to the backend + wha
|
|
|
75
122
|
updateArticleById(request, id, data) PATCH .../blog/article/{id} (same fields) → response
|
|
76
123
|
deleteArticleById(request, id) DELETE .../blog/article/{id} → response
|
|
77
124
|
|
|
78
|
-
- '@webcake/customer' (backend: /cms_function/{site}/customer/…) → customer object ({} if none)
|
|
79
|
-
|
|
125
|
+
- '@webcake/customer' (backend: /cms_function/{site}/customer/…) → customer object ({} if none).
|
|
126
|
+
The object has at least { id, name, avatar, email, phone_number }. ALWAYS check \`customer?.id\`
|
|
127
|
+
before using it (returns {} when not found). Confirmed against a real production function.
|
|
128
|
+
findCustomerById(request, id) GET .../customer/identity/{id}
|
|
80
129
|
findCustomerByPhone(request, phone) GET .../customer/phone/{phone}
|
|
81
130
|
findCustomerByEmail(request, email) GET .../customer/email/{email}
|
|
131
|
+
Typical lookup-by-anything helper: try a code/sku table first, else normalize the phone and
|
|
132
|
+
call findCustomerByPhone, else (has "@") findCustomerByEmail, else findCustomerById.
|
|
82
133
|
|
|
83
|
-
- '@webcake/promotion' (backend: /cms_function/{site}/promotion/add_bonus)
|
|
134
|
+
- '@webcake/promotion' (backend: /cms_function/{site}/promotion/add_bonus) — confirmed in real code
|
|
84
135
|
addBonus(request, data) POST add_bonus → response.
|
|
85
136
|
It ADDS REWARD POINTS to a customer. data: { customer_id (required),
|
|
86
137
|
point (number, required), message? (defaults "Bạn được cộng điểm") }.
|
|
138
|
+
e.g. await addBonus(request, { customer_id: "abc", point: 10, message: "Cộng 10 điểm" }).
|
|
87
139
|
|
|
88
140
|
- '@webcake/token' (backend: /external/oauth/token)
|
|
89
141
|
getAccessToken(request) → access_token string (throws if none).
|
|
@@ -136,18 +188,36 @@ Runs sandboxed: ~4 MB memory, ~30 s timeout. The return value MUST be JSON-seria
|
|
|
136
188
|
## Cron jobs (jobs_config JSON)
|
|
137
189
|
{ "jobs": [{ "functionLocation": "backend/http_function", "functionName": "myFunc", "executionConfig": { "cronExpression": "0 2 * * *" } }] }
|
|
138
190
|
|
|
139
|
-
## Example
|
|
191
|
+
## Example (real-world shape)
|
|
192
|
+
Declare the models + db ONCE at module top, then one export per endpoint. Read the caller from
|
|
193
|
+
request.customer?.id (auth) and the inputs from request.params. Return a plain JSON object —
|
|
194
|
+
the convention is { mess: "OK", ...data } on success or { mess: "ERROR_CODE" } on failure.
|
|
140
195
|
import { DBConnection } from 'webcake-data';
|
|
141
196
|
import { findCustomerById } from '@webcake/customer';
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
197
|
+
|
|
198
|
+
const db = new DBConnection();
|
|
199
|
+
const Members = db.model('thanh_vien_ps');
|
|
200
|
+
|
|
201
|
+
export const post_MyMembers = async (request) => {
|
|
202
|
+
const userId = request.customer?.id ?? ""; // the logged-in storefront customer
|
|
203
|
+
if (!userId) return { mess: "NO_ACCOUNT_CALL" };
|
|
204
|
+
const { playspace = "" } = request.params || {}; // POST body params
|
|
205
|
+
try {
|
|
206
|
+
const rows = await Members
|
|
207
|
+
.find({ playspace, status: { $in: [0, 1, 2] } })
|
|
208
|
+
.select(["id", "thanh_vien", "tien_con_lai", "status"])
|
|
209
|
+
.populate({ field: "thanh_vien", select: ["id", "name", "avatar"] })
|
|
210
|
+
.sort({ status: 1, inserted_at: 1 })
|
|
211
|
+
.limit(50);
|
|
212
|
+
const data = rows.map((row) => {
|
|
213
|
+
const m = row.thanh_vien || {}; // populated object
|
|
214
|
+
return { thanh_vien_ps: row.id, ten: m.name || "", tien: Number(row.tien_con_lai) || 0 };
|
|
215
|
+
});
|
|
216
|
+
return { mess: "OK", data };
|
|
217
|
+
} catch (err) {
|
|
218
|
+
console.error(err?.message || err);
|
|
219
|
+
return { mess: "SYSTEM_ERROR" };
|
|
220
|
+
}
|
|
151
221
|
};
|
|
152
222
|
`;
|
|
153
223
|
export const CUSTOM_CODE_GUIDE = `
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "webcake-storefront-mcp",
|
|
3
|
-
"version": "1.31.
|
|
3
|
+
"version": "1.31.2",
|
|
4
4
|
"description": "MCP server for the WebCake/StoreCake storefront builder — page CRUD, page authoring, products, orders, and more",
|
|
5
5
|
"mcpName": "io.github.vuluu2k/webcake-storefront-mcp",
|
|
6
6
|
"license": "MIT",
|