webcake-storefront-mcp 1.30.0 → 1.31.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/dist/api.js CHANGED
@@ -29,7 +29,7 @@ export class WebcakeCmsApi {
29
29
  getBundleParams() {
30
30
  return { token: this._adminToken, x_cms_api_key: this._cmsApiKey };
31
31
  }
32
- async request(method, path, { body, query, timeout } = {}) {
32
+ async request(method, path, { body, query, timeout, headers: extraHeaders } = {}) {
33
33
  const url = new URL(`${this.baseUrl}${path}`);
34
34
  if (query) {
35
35
  for (const [k, v] of Object.entries(query)) {
@@ -41,6 +41,7 @@ export class WebcakeCmsApi {
41
41
  "Content-Type": "application/json",
42
42
  Authorization: `Bearer ${this.token}`,
43
43
  ...(this.sessionId && { "x-session-id": this.sessionId }),
44
+ ...(extraHeaders || {}),
44
45
  };
45
46
  const controller = new AbortController();
46
47
  const timer = setTimeout(() => controller.abort(), timeout || DEFAULT_TIMEOUT);
@@ -340,8 +341,37 @@ export class WebcakeCmsApi {
340
341
  getCollection(id) {
341
342
  return this.request("GET", `/api/v1/dashboard/site/${this.siteId}/db_collections/${id}`);
342
343
  }
343
- queryCollectionRecords(tableName, query) {
344
- return this.request("GET", `/api/v1/dashboard/site/${this.siteId}/db_collections/collections/${tableName}/records`, { query });
344
+ /** Header carrying the CMS api key — collection-data endpoints (records) need it on top of
345
+ * the dashboard JWT (without it they 401). Fetches the key once and caches it. */
346
+ async cmsApiHeader() {
347
+ await this.fetchCmsTokens();
348
+ return this._cmsApiKey ? { "x-cms-api-key": this._cmsApiKey } : {};
349
+ }
350
+ /** Query a collection's records. Needs the CMS api-key header (else 401). The records
351
+ * endpoint accepts page/limit plus `where` (a filter object/JSON) and `order_by`. */
352
+ async queryCollectionRecords(tableName, query) {
353
+ const headers = await this.cmsApiHeader();
354
+ return this.request("GET", `/api/v1/dashboard/site/${this.siteId}/db_collections/collections/${tableName}/records`, { query, headers });
355
+ }
356
+ /** Insert a record into a collection. Body = the record fields. CMS-api-key authed. */
357
+ async insertCollectionRecord(tableName, record) {
358
+ const headers = await this.cmsApiHeader();
359
+ return this.request("POST", `/api/v1/dashboard/site/${this.siteId}/db_collections/collections/${tableName}/records`, { body: record, headers });
360
+ }
361
+ /** Update a record by id. Body = the changed fields. CMS-api-key authed. */
362
+ async updateCollectionRecord(tableName, recordId, record) {
363
+ const headers = await this.cmsApiHeader();
364
+ return this.request("PATCH", `/api/v1/dashboard/site/${this.siteId}/db_collections/collections/${tableName}/records/${recordId}`, { body: record, headers });
365
+ }
366
+ /** Delete a record by id. CMS-api-key authed. */
367
+ async deleteCollectionRecord(tableName, recordId) {
368
+ const headers = await this.cmsApiHeader();
369
+ return this.request("DELETE", `/api/v1/dashboard/site/${this.siteId}/db_collections/collections/${tableName}/records/${recordId}`, { headers });
370
+ }
371
+ /** Create a collection (table). Body: { name, schema:[{name,type,...}], ... }. */
372
+ async createCollection(params) {
373
+ const headers = await this.cmsApiHeader();
374
+ return this.request("POST", `/api/v1/dashboard/site/${this.siteId}/db_collections`, { body: params, headers, timeout: 60000 });
345
375
  }
346
376
  // ── Blog Articles ──
347
377
  listArticles(query) {
@@ -1,4 +1,18 @@
1
1
  [
2
+ {
3
+ "v": "1.31.1",
4
+ "d": "26/06/2026",
5
+ "type": "Fixed",
6
+ "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…",
7
+ "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…"
8
+ },
9
+ {
10
+ "v": "1.31.0",
11
+ "d": "26/06/2026",
12
+ "type": "Added",
13
+ "en": "New create_collection tool creates a custom data table by accepting a name and a schema array of field definitions (with types such as string,…",
14
+ "vi": "Tool mới create_collection tạo bảng dữ liệu tùy chỉnh bằng cách nhận tham số name và mảng schema chứa định nghĩa các trường (với các kiểu dữ liệu…"
15
+ },
2
16
  {
3
17
  "v": "1.30.0",
4
18
  "d": "26/06/2026",
@@ -26,19 +40,5 @@
26
40
  "type": "Added",
27
41
  "en": "build_page now accepts an seo object (title, description, keyword, favicon, thumbnail) that is written to page.settings.seo with Open Graph…",
28
42
  "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ỗ…"
29
- },
30
- {
31
- "v": "1.26.1",
32
- "d": "26/06/2026",
33
- "type": "Fixed",
34
- "en": "list_products now correctly unwraps the { products, total_product } API response shape, returning a product array and accurate total instead of…",
35
- "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…"
36
- },
37
- {
38
- "v": "1.26.0",
39
- "d": "26/06/2026",
40
- "type": "Fixed",
41
- "en": "create_product now accepts a short_description parameter and sends it to the backend as [{description}] — the array shape the real product schema…",
42
- "vi": "create_product nay nhận thêm tham số short_description và gửi lên backend dưới dạng [{description}] — đúng kiểu mảng mà schema sản phẩm thực tế yêu…"
43
43
  }
44
44
  ]
package/dist/guides.js CHANGED
@@ -22,38 +22,59 @@ 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
- ### Model CRUD (all async unless noted)
31
- - Model.create(doc) created doc
32
- - Model.insertMany([doc, ...]) array
33
- - Model.find(filter) → QueryBuilder (NOT a promise — chain then .exec()/await)
34
- - Model.findOne(filter, { select, sort, populate })
35
- - Model.findById(id, { select, populate })
36
- - Model.updateOne(filter, update) → { acknowledged, matchedCount, modifiedCount }
37
- - Model.findByIdAndUpdate(id, update, { new: true })
38
- - Model.findOneAndUpdate(filter, update)
39
- - Model.updateMany(filter, update)
40
- - Model.deleteOne(filter) → { acknowledged, deletedCount }
41
- - Model.findByIdAndDelete(id) / Model.findOneAndDelete(filter)
42
- - Model.deleteMany(filter)
43
- - Model.countDocuments(filter) → number
44
- - Model.exists(filter) → boolean
45
-
46
- ### QueryBuilder (from Model.find())
47
- Chain then terminate with .exec() (or just await the chain):
48
- Model.find().where('age').gte(25).lte(40).in('role',['admin']).like('email','%@ex.com')
49
- .sort({ age:-1 }).limit(20).skip(10).select('name email').exec()
50
- Operators: where, eq, ne, gt, gte, lt, lte, in, nin, between, like, sort, limit, skip, select, populate.
51
-
52
- ### Populate (join another collection)
53
- Model.find().populate({
54
- field:'posts', table:'posts', referenceField:'user_id',
55
- select:'title', where:{}, sort:{ created_at:-1 }, limit:5, skip:0, justOne:false
56
- }).exec()
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 });
57
78
 
58
79
  ## Built-in @webcake/* modules (first arg is always request; they auth via global.token)
59
80
  Thin wrappers over the backend's /cms_function/{site_id}/... endpoints. Pass request so
@@ -136,18 +157,36 @@ Runs sandboxed: ~4 MB memory, ~30 s timeout. The return value MUST be JSON-seria
136
157
  ## Cron jobs (jobs_config JSON)
137
158
  { "jobs": [{ "functionLocation": "backend/http_function", "functionName": "myFunc", "executionConfig": { "cronExpression": "0 2 * * *" } }] }
138
159
 
139
- ## Example
160
+ ## Example (real-world shape)
161
+ Declare the models + db ONCE at module top, then one export per endpoint. Read the caller from
162
+ request.customer?.id (auth) and the inputs from request.params. Return a plain JSON object —
163
+ the convention is { mess: "OK", ...data } on success or { mess: "ERROR_CODE" } on failure.
140
164
  import { DBConnection } from 'webcake-data';
141
165
  import { findCustomerById } from '@webcake/customer';
142
- export const post_RecentOrders = async (request) => {
143
- const { params, customer } = request;
144
- const db = new DBConnection();
145
- const orders = await db.model('orders')
146
- .find().where('customer_id').eq(customer.id || params.customer_id)
147
- .sort({ created_at:-1 }).limit(10)
148
- .populate({ field:'items', table:'order_items', referenceField:'order_id', limit:50 })
149
- .exec();
150
- return { count: orders.length, orders };
166
+
167
+ const db = new DBConnection();
168
+ const Members = db.model('thanh_vien_ps');
169
+
170
+ export const post_MyMembers = async (request) => {
171
+ const userId = request.customer?.id ?? ""; // the logged-in storefront customer
172
+ if (!userId) return { mess: "NO_ACCOUNT_CALL" };
173
+ const { playspace = "" } = request.params || {}; // POST body params
174
+ try {
175
+ const rows = await Members
176
+ .find({ playspace, status: { $in: [0, 1, 2] } })
177
+ .select(["id", "thanh_vien", "tien_con_lai", "status"])
178
+ .populate({ field: "thanh_vien", select: ["id", "name", "avatar"] })
179
+ .sort({ status: 1, inserted_at: 1 })
180
+ .limit(50);
181
+ const data = rows.map((row) => {
182
+ const m = row.thanh_vien || {}; // populated object
183
+ return { thanh_vien_ps: row.id, ten: m.name || "", tien: Number(row.tien_con_lai) || 0 };
184
+ });
185
+ return { mess: "OK", data };
186
+ } catch (err) {
187
+ console.error(err?.message || err);
188
+ return { mess: "SYSTEM_ERROR" };
189
+ }
151
190
  };
152
191
  `;
153
192
  export const CUSTOM_CODE_GUIDE = `
@@ -25,9 +25,40 @@ export function registerCollectionTools(server, api, handle) {
25
25
  server.tool("get_collection", "Get a specific collection's details including full schema (field names, types, constraints, references) and records", {
26
26
  id: z.string().describe("Collection ID"),
27
27
  }, ({ id }) => handle(() => api.getCollection(id)));
28
- server.tool("query_collection_records", "Query records from a collection by table name. Use to inspect existing data", {
29
- table_name: z.string().describe("Collection table name (e.g. 'subscribers', 'custom_orders')"),
28
+ server.tool("query_collection_records", "Query records from a collection (custom data table) by table name. Supports paging + an optional `where` filter and `order_by` sort. (Uses the CMS api-key auth the records endpoint requires.)", {
29
+ table_name: z.string().describe("Collection table name (e.g. 'subscribers', 'custom_orders') — from get_collection.table_name"),
30
30
  page: z.number().optional().describe("Page number"),
31
31
  limit: z.number().optional().describe("Items per page"),
32
- }, ({ table_name, page, limit }) => handle(() => api.queryCollectionRecords(table_name, { page, limit })));
32
+ where: z.record(z.any()).optional().describe("Filter object, e.g. { status: 'active' } matches records by field value."),
33
+ order_by: z.string().optional().describe("Field to sort by (e.g. 'inserted_at')."),
34
+ }, ({ table_name, page, limit, where, order_by }) => handle(() => {
35
+ const query = { page: page ?? 1, limit: limit ?? 50 };
36
+ if (where && Object.keys(where).length)
37
+ query.where = JSON.stringify(where);
38
+ if (order_by)
39
+ query.order_by = order_by;
40
+ return api.queryCollectionRecords(table_name, query);
41
+ }));
42
+ // ── Write tools (custom data CRUD). Endpoints follow the same /db_collections/collections/
43
+ // {table}/records[/{id}] REST pattern as the verified read endpoint, with the CMS api-key
44
+ // header. They mutate live data — run a small test first to confirm your field shape. ──
45
+ server.tool("create_collection", "Create a new collection (custom data TABLE) so you can store/query arbitrary data. Pass a name + the field schema. The backend adds id/inserted_at/updated_at automatically.", {
46
+ name: z.string().describe("Collection / table name (e.g. 'subscribers')."),
47
+ schema: z
48
+ .array(z.object({ name: z.string(), type: z.string().describe("Field type: string | text | integer | float | boolean | naive_datetime | binary_id | map | array"), required: z.boolean().optional() }))
49
+ .describe("Field definitions, e.g. [{name:'email',type:'string',required:true},{name:'joined_at',type:'naive_datetime'}]."),
50
+ }, ({ name, schema }) => handle(() => api.createCollection({ name, schema })));
51
+ server.tool("insert_collection_record", "Insert a record into a collection (custom data table). `record` is a field→value object matching the table schema.", {
52
+ table_name: z.string().describe("Collection table name."),
53
+ record: z.record(z.any()).describe("Record fields, e.g. { email:'a@b.com', joined_at:'2026-06-26T10:00:00' }."),
54
+ }, ({ table_name, record }) => handle(() => api.insertCollectionRecord(table_name, record)));
55
+ server.tool("update_collection_record", "Update a record in a collection by id. `record` carries only the changed fields.", {
56
+ table_name: z.string().describe("Collection table name."),
57
+ record_id: z.string().describe("Record id to update."),
58
+ record: z.record(z.any()).describe("Changed fields."),
59
+ }, ({ table_name, record_id, record }) => handle(() => api.updateCollectionRecord(table_name, record_id, record)));
60
+ server.tool("delete_collection_record", "Delete a record from a collection by id.", {
61
+ table_name: z.string().describe("Collection table name."),
62
+ record_id: z.string().describe("Record id to delete."),
63
+ }, ({ table_name, record_id }) => handle(() => api.deleteCollectionRecord(table_name, record_id)));
33
64
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "webcake-storefront-mcp",
3
- "version": "1.30.0",
3
+ "version": "1.31.1",
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",