mirror of
https://github.com/RaisFast/raisfast.git
synced 2026-09-24 08:02:36 +00:00
add database crud operation function to plugin system
This commit is contained in:
@@ -1,49 +0,0 @@
|
||||
[content_type]
|
||||
name = "Cart Item"
|
||||
singular = "cart_item"
|
||||
plural = "cart_items"
|
||||
table = "cart_items"
|
||||
description = "购物车项"
|
||||
implements = ["ownable", "timestampable"]
|
||||
|
||||
[fields.user_id]
|
||||
type = "text"
|
||||
required = true
|
||||
label = "用户ID"
|
||||
|
||||
[fields.product_id]
|
||||
type = "text"
|
||||
required = true
|
||||
label = "商品ID"
|
||||
|
||||
[fields.quantity]
|
||||
type = "integer"
|
||||
required = true
|
||||
min = 1
|
||||
default = 1
|
||||
label = "数量"
|
||||
|
||||
[[indexes]]
|
||||
fields = ["user_id", "product_id"]
|
||||
unique = true
|
||||
|
||||
[list_view]
|
||||
default_sort = "created_at:desc"
|
||||
columns = ["user_id", "product_id", "quantity", "created_at"]
|
||||
|
||||
[api.list]
|
||||
access = "admin"
|
||||
cache = true
|
||||
|
||||
[api.get]
|
||||
access = "admin"
|
||||
cache = true
|
||||
|
||||
[api.create]
|
||||
access = "admin"
|
||||
|
||||
[api.update]
|
||||
access = "admin"
|
||||
|
||||
[api.delete]
|
||||
access = "admin"
|
||||
@@ -1,79 +0,0 @@
|
||||
[content_type]
|
||||
name = "Order"
|
||||
singular = "order"
|
||||
plural = "orders"
|
||||
table = "orders"
|
||||
description = "订单"
|
||||
implements = ["ownable", "timestampable"]
|
||||
|
||||
[fields.order_no]
|
||||
type = "text"
|
||||
required = true
|
||||
unique = true
|
||||
max_length = 32
|
||||
immutable = true
|
||||
label = "订单号"
|
||||
|
||||
[fields.user_id]
|
||||
type = "text"
|
||||
required = true
|
||||
immutable = true
|
||||
label = "用户ID"
|
||||
|
||||
[fields.status]
|
||||
type = "enum"
|
||||
enum_values = ["pending", "paid", "shipped", "completed", "cancelled", "refunded"]
|
||||
default = "pending"
|
||||
label = "订单状态"
|
||||
|
||||
[fields.total_amount]
|
||||
type = "decimal"
|
||||
required = true
|
||||
min = 0
|
||||
label = "订单总额"
|
||||
|
||||
[fields.shipping_address]
|
||||
type = "json"
|
||||
label = "收货地址"
|
||||
|
||||
[fields.note]
|
||||
type = "text"
|
||||
max_length = 500
|
||||
label = "备注"
|
||||
|
||||
[fields.paid_at]
|
||||
type = "datetime"
|
||||
label = "支付时间"
|
||||
|
||||
[fields.shipped_at]
|
||||
type = "datetime"
|
||||
label = "发货时间"
|
||||
|
||||
[[indexes]]
|
||||
fields = ["order_no"]
|
||||
unique = true
|
||||
|
||||
[[indexes]]
|
||||
fields = ["user_id", "created_at"]
|
||||
|
||||
[list_view]
|
||||
default_sort = "created_at:desc"
|
||||
columns = ["order_no", "user_id", "status", "total_amount", "created_at"]
|
||||
|
||||
[api.list]
|
||||
access = "admin"
|
||||
cache = true
|
||||
filter_auth = 'user_id = @request.auth.id'
|
||||
|
||||
[api.get]
|
||||
access = "admin"
|
||||
cache = true
|
||||
|
||||
[api.create]
|
||||
access = "admin"
|
||||
|
||||
[api.update]
|
||||
access = "admin"
|
||||
|
||||
[api.delete]
|
||||
access = "admin"
|
||||
@@ -1,70 +0,0 @@
|
||||
[content_type]
|
||||
name = "Order Item"
|
||||
singular = "order_item"
|
||||
plural = "order_items"
|
||||
table = "order_items"
|
||||
description = "订单项"
|
||||
implements = ["ownable", "timestampable"]
|
||||
|
||||
[fields.order_id]
|
||||
type = "relation"
|
||||
relation_type = "many_to_one"
|
||||
target = "orders"
|
||||
foreign_key = "order_id"
|
||||
required = true
|
||||
immutable = true
|
||||
label = "订单"
|
||||
|
||||
[fields.product_id]
|
||||
type = "text"
|
||||
required = true
|
||||
immutable = true
|
||||
label = "商品ID"
|
||||
|
||||
[fields.product_name]
|
||||
type = "text"
|
||||
required = true
|
||||
max_length = 200
|
||||
immutable = true
|
||||
label = "商品名称"
|
||||
|
||||
[fields.price]
|
||||
type = "decimal"
|
||||
required = true
|
||||
min = 0
|
||||
immutable = true
|
||||
label = "单价"
|
||||
|
||||
[fields.quantity]
|
||||
type = "integer"
|
||||
required = true
|
||||
min = 1
|
||||
immutable = true
|
||||
label = "数量"
|
||||
|
||||
[fields.subtotal]
|
||||
type = "decimal"
|
||||
required = true
|
||||
min = 0
|
||||
immutable = true
|
||||
label = "小计"
|
||||
|
||||
[[indexes]]
|
||||
fields = ["order_id"]
|
||||
|
||||
[api.list]
|
||||
access = "admin"
|
||||
cache = true
|
||||
|
||||
[api.get]
|
||||
access = "admin"
|
||||
cache = true
|
||||
|
||||
[api.create]
|
||||
access = "admin"
|
||||
|
||||
[api.update]
|
||||
access = "admin"
|
||||
|
||||
[api.delete]
|
||||
access = "admin"
|
||||
@@ -1,99 +0,0 @@
|
||||
[content_type]
|
||||
name = "Product"
|
||||
singular = "product"
|
||||
plural = "products"
|
||||
table = "products"
|
||||
description = "商品"
|
||||
implements = ["ownable", "timestampable"]
|
||||
slug_field = "name"
|
||||
|
||||
[fields.name]
|
||||
type = "text"
|
||||
required = true
|
||||
max_length = 200
|
||||
label = "商品名称"
|
||||
|
||||
[fields.slug]
|
||||
type = "uid"
|
||||
target_field = "name"
|
||||
unique = true
|
||||
label = "URL 标识"
|
||||
|
||||
[fields.description]
|
||||
type = "richtext"
|
||||
label = "商品描述"
|
||||
|
||||
[fields.price]
|
||||
type = "decimal"
|
||||
required = true
|
||||
min = 0
|
||||
label = "价格"
|
||||
|
||||
[fields.compare_at_price]
|
||||
type = "decimal"
|
||||
min = 0
|
||||
label = "划线价"
|
||||
|
||||
[fields.stock]
|
||||
type = "integer"
|
||||
required = true
|
||||
min = 0
|
||||
default = 0
|
||||
label = "库存"
|
||||
|
||||
[fields.sku]
|
||||
type = "text"
|
||||
unique = true
|
||||
max_length = 50
|
||||
label = "SKU"
|
||||
|
||||
[fields.images]
|
||||
type = "media"
|
||||
accept = ["image/*"]
|
||||
max_count = 10
|
||||
label = "商品图片"
|
||||
|
||||
[fields.category]
|
||||
type = "relation"
|
||||
relation_type = "many_to_one"
|
||||
target = "product_categories"
|
||||
foreign_key = "category_id"
|
||||
label = "分类"
|
||||
|
||||
[fields.featured]
|
||||
type = "boolean"
|
||||
default = false
|
||||
label = "推荐商品"
|
||||
|
||||
[fields.weight]
|
||||
type = "decimal"
|
||||
min = 0
|
||||
label = "重量(kg)"
|
||||
|
||||
[[indexes]]
|
||||
fields = ["slug"]
|
||||
unique = true
|
||||
|
||||
[[indexes]]
|
||||
fields = ["status", "created_at"]
|
||||
|
||||
[list_view]
|
||||
default_sort = "created_at:desc"
|
||||
columns = ["name", "price", "stock", "status", "featured", "created_at"]
|
||||
|
||||
[api.list]
|
||||
access = "public"
|
||||
cache = true
|
||||
|
||||
[api.get]
|
||||
access = "public"
|
||||
cache = true
|
||||
|
||||
[api.create]
|
||||
access = "admin"
|
||||
|
||||
[api.update]
|
||||
access = "admin"
|
||||
|
||||
[api.delete]
|
||||
access = "admin"
|
||||
@@ -1,68 +0,0 @@
|
||||
[content_type]
|
||||
name = "Product Category"
|
||||
singular = "product_category"
|
||||
plural = "product_categories"
|
||||
table = "product_categories"
|
||||
description = "商品分类"
|
||||
implements = ["ownable", "timestampable"]
|
||||
slug_field = "name"
|
||||
|
||||
[fields.name]
|
||||
type = "text"
|
||||
required = true
|
||||
max_length = 100
|
||||
label = "分类名称"
|
||||
|
||||
[fields.slug]
|
||||
type = "uid"
|
||||
target_field = "name"
|
||||
unique = true
|
||||
label = "URL 标识"
|
||||
|
||||
[fields.description]
|
||||
type = "text"
|
||||
max_length = 500
|
||||
label = "描述"
|
||||
|
||||
[fields.parent]
|
||||
type = "relation"
|
||||
relation_type = "many_to_one"
|
||||
target = "product_categories"
|
||||
foreign_key = "parent_id"
|
||||
label = "父分类"
|
||||
|
||||
[fields.sort_order]
|
||||
type = "integer"
|
||||
default = 0
|
||||
label = "排序"
|
||||
|
||||
[fields.image]
|
||||
type = "media"
|
||||
accept = ["image/*"]
|
||||
max_count = 1
|
||||
label = "分类图片"
|
||||
|
||||
[[indexes]]
|
||||
fields = ["slug"]
|
||||
unique = true
|
||||
|
||||
[list_view]
|
||||
default_sort = "sort_order:asc,created_at:desc"
|
||||
columns = ["name", "slug", "sort_order", "created_at"]
|
||||
|
||||
[api.list]
|
||||
access = "public"
|
||||
cache = true
|
||||
|
||||
[api.get]
|
||||
access = "public"
|
||||
cache = true
|
||||
|
||||
[api.create]
|
||||
access = "admin"
|
||||
|
||||
[api.update]
|
||||
access = "admin"
|
||||
|
||||
[api.delete]
|
||||
access = "admin"
|
||||
+97
-158
@@ -1,4 +1,6 @@
|
||||
import { dbQuery, dbExec, ok, fail, extractJson, logInfo, eventEmit, newId } from 'sdk';
|
||||
import { dbQuery, dbExec, ok, fail, extractJson, logInfo, eventEmit, newId,
|
||||
dbInsert, dbFetchOne, dbFetchAll, dbUpdate, dbDelete, dbCount,
|
||||
dbIncrement, dbSum, dbGroupBy } from 'sdk';
|
||||
|
||||
const nowISO = () => new Date().toISOString();
|
||||
|
||||
@@ -67,18 +69,9 @@ export function getPipeline(input) {
|
||||
|
||||
const pipeline = [];
|
||||
for (const stage of stages) {
|
||||
const rows = dbQuery(
|
||||
`SELECT id, title, amount, currency, probability, contact_id, company_id, owner_id, close_date
|
||||
FROM crm_deals WHERE stage = ? ORDER BY amount DESC`,
|
||||
[stage]
|
||||
);
|
||||
const totalResult = dbQuery(
|
||||
`SELECT CAST(COALESCE(SUM(amount), 0) AS TEXT) as total, CAST(COUNT(*) AS TEXT) as cnt
|
||||
FROM crm_deals WHERE stage = ?`,
|
||||
[stage]
|
||||
);
|
||||
const total = totalResult?.[0] ? parseFloat(totalResult[0].total) : 0;
|
||||
const count = totalResult?.[0] ? parseInt(totalResult[0].cnt, 10) : 0;
|
||||
const rows = dbFetchAll("crm_deals", { stage }, { order_by: "amount DESC" }).data || [];
|
||||
const total = dbSum("crm_deals", "amount", { stage });
|
||||
const count = dbCount("crm_deals", { stage });
|
||||
const weighted = total * (stage === "closed_won" ? 100 : stage === "closed_lost" ? 0 : 30) / 100;
|
||||
|
||||
pipeline.push({
|
||||
@@ -87,7 +80,7 @@ export function getPipeline(input) {
|
||||
count,
|
||||
total_value: total,
|
||||
weighted_value: weighted,
|
||||
deals: rows || [],
|
||||
deals: rows,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -110,36 +103,21 @@ export function getDealDetail(input) {
|
||||
const dealId = extractJson(input, "params.dealId");
|
||||
if (!dealId) return fail(400, "deal id required");
|
||||
|
||||
const deals = dbQuery(
|
||||
`SELECT id, title, amount, currency, stage, probability,
|
||||
contact_id, company_id, owner_id, close_date, loss_reason, description,
|
||||
created_at, updated_at
|
||||
FROM crm_deals WHERE id = ?`,
|
||||
[dealId]
|
||||
);
|
||||
if (!deals || deals.length === 0) return fail(404, "deal not found");
|
||||
const deal = dbFetchOne("crm_deals", { id: dealId });
|
||||
if (!deal) return fail(404, "deal not found");
|
||||
|
||||
const deal = deals[0];
|
||||
deal.amount = parseFloat(deal.amount) || 0;
|
||||
deal.probability = parseIntOrNull(deal.probability) || 0;
|
||||
|
||||
const activities = dbQuery(
|
||||
`SELECT id, type, subject, activity_date, outcome, duration_minutes, owner_id, created_at
|
||||
FROM crm_activities WHERE deal_id = ? ORDER BY activity_date DESC LIMIT 20`,
|
||||
[dealId]
|
||||
);
|
||||
for (const a of (activities || [])) {
|
||||
const activities = dbFetchAll("crm_activities", { deal_id: dealId }, { order_by: "activity_date DESC", limit: 20 }).data || [];
|
||||
for (const a of activities) {
|
||||
a.duration_minutes = parseIntOrNull(a.duration_minutes);
|
||||
}
|
||||
|
||||
const notes = dbQuery(
|
||||
`SELECT id, content, pinned, owner_id, created_at
|
||||
FROM crm_notes WHERE deal_id = ? ORDER BY pinned DESC, created_at DESC LIMIT 20`,
|
||||
[dealId]
|
||||
);
|
||||
const notes = dbFetchAll("crm_notes", { deal_id: dealId }, { order_by: "pinned DESC, created_at DESC", limit: 20 }).data || [];
|
||||
|
||||
deal.activities = activities || [];
|
||||
deal.notes = notes || [];
|
||||
deal.activities = activities;
|
||||
deal.notes = notes;
|
||||
|
||||
return ok(deal);
|
||||
}
|
||||
@@ -161,49 +139,40 @@ export function updateDealStage(input) {
|
||||
const validStages = ["prospecting", "qualification", "proposal", "negotiation", "closed_won", "closed_lost"];
|
||||
if (!validStages.includes(newStage)) return fail(400, `invalid stage: ${newStage}`);
|
||||
|
||||
const deals = dbQuery("SELECT id, stage, title FROM crm_deals WHERE id = ?", [dealId]);
|
||||
if (!deals || deals.length === 0) return fail(404, "deal not found");
|
||||
const deal = dbFetchOne("crm_deals", { id: dealId });
|
||||
if (!deal) return fail(404, "deal not found");
|
||||
|
||||
const oldStage = deals[0].stage;
|
||||
const oldStage = deal.stage;
|
||||
if (oldStage === newStage) return fail(400, "deal already in this stage");
|
||||
|
||||
const now = nowISO();
|
||||
const updates = ["stage = ?", "updated_at = ?"];
|
||||
const params = [newStage, now];
|
||||
const setData = { stage: newStage, updated_at: now };
|
||||
|
||||
if (probability != null) {
|
||||
updates.push("probability = ?");
|
||||
params.push(String(probability));
|
||||
setData.probability = String(probability);
|
||||
}
|
||||
|
||||
if (newStage === "closed_won") {
|
||||
updates.push("probability = ?");
|
||||
params.push("100");
|
||||
setData.probability = "100";
|
||||
}
|
||||
|
||||
if (newStage === "closed_lost") {
|
||||
updates.push("probability = ?");
|
||||
params.push("0");
|
||||
setData.probability = "0";
|
||||
if (lossReason) {
|
||||
updates.push("loss_reason = ?");
|
||||
params.push(lossReason);
|
||||
setData.loss_reason = lossReason;
|
||||
}
|
||||
}
|
||||
|
||||
params.push(dealId);
|
||||
const r = dbExec(`UPDATE crm_deals SET ${updates.join(", ")} WHERE id = ?`, params);
|
||||
const r = dbUpdate("crm_deals", setData, { id: dealId });
|
||||
if (r.error) return fail(500, r.error);
|
||||
|
||||
const activityId = newId();
|
||||
const activityContent = JSON.stringify({ old_stage: oldStage, new_stage: newStage });
|
||||
dbExec(
|
||||
"INSERT INTO crm_activities (id, tenant_id, type, subject, content, deal_id, owner_id, activity_date, created_at, updated_at) VALUES (?, 'default', 'note', ?, ?, ?, ?, ?, ?, ?)",
|
||||
[activityId, `Stage: ${oldStage} → ${newStage}`, activityContent, dealId, userId, now, now, now]
|
||||
);
|
||||
dbInsert("crm_activities", { id: activityId, tenant_id: "default", type: "note", subject: `Stage: ${oldStage} → ${newStage}`, content: activityContent, deal_id: dealId, owner_id: userId, activity_date: now, created_at: now, updated_at: now });
|
||||
|
||||
eventEmit("crm.deal_stage_changed", JSON.stringify({
|
||||
deal_id: dealId,
|
||||
deal_title: deals[0].title,
|
||||
deal_title: deal.title,
|
||||
old_stage: oldStage,
|
||||
new_stage: newStage,
|
||||
changed_by: userId,
|
||||
@@ -223,28 +192,19 @@ export function getContactTimeline(input) {
|
||||
const contactId = extractJson(input, "params.contactId");
|
||||
if (!contactId) return fail(400, "contact id required");
|
||||
|
||||
const contacts = dbQuery("SELECT id FROM crm_contacts WHERE id = ?", [contactId]);
|
||||
if (!contacts || contacts.length === 0) return fail(404, "contact not found");
|
||||
const contact = dbFetchOne("crm_contacts", { id: contactId });
|
||||
if (!contact) return fail(404, "contact not found");
|
||||
|
||||
const activities = dbQuery(
|
||||
`SELECT 'activity' as type, id, type as activity_type, subject, content, activity_date, outcome, duration_minutes, owner_id, created_at
|
||||
FROM crm_activities WHERE contact_id = ? ORDER BY activity_date DESC LIMIT 50`,
|
||||
[contactId]
|
||||
);
|
||||
|
||||
const notes = dbQuery(
|
||||
`SELECT 'note' as type, id, content, pinned, owner_id, created_at
|
||||
FROM crm_notes WHERE contact_id = ? ORDER BY created_at DESC LIMIT 50`,
|
||||
[contactId]
|
||||
);
|
||||
const rawActivities = dbFetchAll("crm_activities", { contact_id: contactId }, { order_by: "activity_date DESC", limit: 50 }).data || [];
|
||||
const rawNotes = dbFetchAll("crm_notes", { contact_id: contactId }, { order_by: "created_at DESC", limit: 50 }).data || [];
|
||||
|
||||
const timeline = [];
|
||||
for (const a of (activities || [])) {
|
||||
for (const a of rawActivities) {
|
||||
a.duration_minutes = parseIntOrNull(a.duration_minutes);
|
||||
timeline.push({ ...a, timestamp: a.activity_date || a.created_at });
|
||||
timeline.push({ ...a, activity_type: a.type, type: 'activity', timestamp: a.activity_date || a.created_at });
|
||||
}
|
||||
for (const n of (notes || [])) {
|
||||
timeline.push({ ...n, activity_type: "note", timestamp: n.created_at });
|
||||
for (const n of rawNotes) {
|
||||
timeline.push({ ...n, activity_type: "note", type: 'note', timestamp: n.created_at });
|
||||
}
|
||||
|
||||
timeline.sort((a, b) => (b.timestamp || "").localeCompare(a.timestamp || ""));
|
||||
@@ -258,37 +218,23 @@ export function getCompanyTimeline(input) {
|
||||
const companyId = extractJson(input, "params.companyId");
|
||||
if (!companyId) return fail(400, "company id required");
|
||||
|
||||
const companies = dbQuery("SELECT id FROM crm_companies WHERE id = ?", [companyId]);
|
||||
if (!companies || companies.length === 0) return fail(404, "company not found");
|
||||
const company = dbFetchOne("crm_companies", { id: companyId });
|
||||
if (!company) return fail(404, "company not found");
|
||||
|
||||
const activities = dbQuery(
|
||||
`SELECT 'activity' as type, id, type as activity_type, subject, content, activity_date, outcome, owner_id, created_at
|
||||
FROM crm_activities WHERE company_id = ? ORDER BY activity_date DESC LIMIT 50`,
|
||||
[companyId]
|
||||
);
|
||||
|
||||
const notes = dbQuery(
|
||||
`SELECT 'note' as type, id, content, pinned, owner_id, created_at
|
||||
FROM crm_notes WHERE company_id = ? ORDER BY created_at DESC LIMIT 50`,
|
||||
[companyId]
|
||||
);
|
||||
|
||||
const deals = dbQuery(
|
||||
`SELECT 'deal' as type, id, title, stage, amount, currency, close_date, created_at
|
||||
FROM crm_deals WHERE company_id = ? ORDER BY created_at DESC LIMIT 20`,
|
||||
[companyId]
|
||||
);
|
||||
const rawActivities = dbFetchAll("crm_activities", { company_id: companyId }, { order_by: "activity_date DESC", limit: 50 }).data || [];
|
||||
const rawNotes = dbFetchAll("crm_notes", { company_id: companyId }, { order_by: "created_at DESC", limit: 50 }).data || [];
|
||||
const rawDeals = dbFetchAll("crm_deals", { company_id: companyId }, { order_by: "created_at DESC", limit: 20 }).data || [];
|
||||
|
||||
const timeline = [];
|
||||
for (const a of (activities || [])) {
|
||||
timeline.push({ ...a, timestamp: a.activity_date || a.created_at });
|
||||
for (const a of rawActivities) {
|
||||
timeline.push({ ...a, activity_type: a.type, type: 'activity', timestamp: a.activity_date || a.created_at });
|
||||
}
|
||||
for (const n of (notes || [])) {
|
||||
timeline.push({ ...n, activity_type: "note", timestamp: n.created_at });
|
||||
for (const n of rawNotes) {
|
||||
timeline.push({ ...n, activity_type: "note", type: 'note', timestamp: n.created_at });
|
||||
}
|
||||
for (const d of (deals || [])) {
|
||||
for (const d of rawDeals) {
|
||||
d.amount = parseFloat(d.amount) || 0;
|
||||
timeline.push({ ...d, activity_type: "deal", timestamp: d.created_at });
|
||||
timeline.push({ ...d, activity_type: "deal", type: 'deal', timestamp: d.created_at });
|
||||
}
|
||||
|
||||
timeline.sort((a, b) => (b.timestamp || "").localeCompare(a.timestamp || ""));
|
||||
@@ -299,25 +245,17 @@ export function getCompanyTimeline(input) {
|
||||
// ── GET /stats ───────────────────────────────────────────────
|
||||
|
||||
export function getDashboardStats() {
|
||||
const contactCount = dbQuery("SELECT CAST(COUNT(*) AS TEXT) as cnt FROM crm_contacts");
|
||||
const totalContacts = contactCount?.[0] ? parseInt(contactCount[0].cnt, 10) : 0;
|
||||
const totalContacts = dbCount("crm_contacts");
|
||||
const activeCount = dbCount("crm_contacts", { status: "active" });
|
||||
const totalCompanies = dbCount("crm_companies");
|
||||
|
||||
const activeContacts = dbQuery("SELECT CAST(COUNT(*) AS TEXT) as cnt FROM crm_contacts WHERE status = 'active'");
|
||||
const activeCount = activeContacts?.[0] ? parseInt(activeContacts[0].cnt, 10) : 0;
|
||||
const openDealCount = dbCount("crm_deals", "stage NOT IN ('closed_won', 'closed_lost')");
|
||||
const openDealValue = dbSum("crm_deals", "amount", "stage NOT IN ('closed_won', 'closed_lost')");
|
||||
|
||||
const companyCount = dbQuery("SELECT CAST(COUNT(*) AS TEXT) as cnt FROM crm_companies");
|
||||
const totalCompanies = companyCount?.[0] ? parseInt(companyCount[0].cnt, 10) : 0;
|
||||
const wonDealCount = dbCount("crm_deals", { stage: "closed_won" });
|
||||
const wonDealValue = dbSum("crm_deals", "amount", { stage: "closed_won" });
|
||||
|
||||
const openDeals = dbQuery("SELECT CAST(COUNT(*) AS TEXT) as cnt, CAST(COALESCE(SUM(amount), 0) AS TEXT) as total FROM crm_deals WHERE stage NOT IN ('closed_won', 'closed_lost')");
|
||||
const openDealCount = openDeals?.[0] ? parseInt(openDeals[0].cnt, 10) : 0;
|
||||
const openDealValue = openDeals?.[0] ? parseFloat(openDeals[0].total) : 0;
|
||||
|
||||
const wonDeals = dbQuery("SELECT CAST(COUNT(*) AS TEXT) as cnt, CAST(COALESCE(SUM(amount), 0) AS TEXT) as total FROM crm_deals WHERE stage = 'closed_won'");
|
||||
const wonDealCount = wonDeals?.[0] ? parseInt(wonDeals[0].cnt, 10) : 0;
|
||||
const wonDealValue = wonDeals?.[0] ? parseFloat(wonDeals[0].total) : 0;
|
||||
|
||||
const lostDeals = dbQuery("SELECT CAST(COUNT(*) AS TEXT) as cnt FROM crm_deals WHERE stage = 'closed_lost'");
|
||||
const lostDealCount = lostDeals?.[0] ? parseInt(lostDeals[0].cnt, 10) : 0;
|
||||
const lostDealCount = dbCount("crm_deals", { stage: "closed_lost" });
|
||||
|
||||
const winRate = (wonDealCount + lostDealCount) > 0
|
||||
? Math.round((wonDealCount / (wonDealCount + lostDealCount)) * 100)
|
||||
@@ -330,11 +268,13 @@ export function getDashboardStats() {
|
||||
);
|
||||
const weeklyActivities = activityThisWeek?.[0] ? parseInt(activityThisWeek[0].cnt, 10) : 0;
|
||||
|
||||
const contactsByStage = dbQuery(
|
||||
`SELECT lifecycle_stage, CAST(COUNT(*) AS TEXT) as cnt FROM crm_contacts GROUP BY lifecycle_stage ORDER BY cnt DESC`
|
||||
);
|
||||
const lifecycleResult = dbGroupBy("crm_contacts", {
|
||||
group_by: "lifecycle_stage",
|
||||
count: true,
|
||||
order_by: "cnt DESC"
|
||||
});
|
||||
const lifecycleDistribution = {};
|
||||
for (const row of (contactsByStage || [])) {
|
||||
for (const row of (lifecycleResult.data || [])) {
|
||||
lifecycleDistribution[row.lifecycle_stage] = parseInt(row.cnt, 10);
|
||||
}
|
||||
|
||||
@@ -358,31 +298,37 @@ export function getDashboardStats() {
|
||||
// ── GET /leaderboard ─────────────────────────────────────────
|
||||
|
||||
export function getLeaderboard() {
|
||||
const byDealValue = dbQuery(
|
||||
`SELECT owner_id, CAST(COUNT(*) AS TEXT) as deal_count, CAST(COALESCE(SUM(amount), 0) AS TEXT) as total_value
|
||||
FROM crm_deals WHERE stage = 'closed_won' GROUP BY owner_id ORDER BY total_value DESC LIMIT 10`
|
||||
);
|
||||
const dealResult = dbGroupBy("crm_deals", {
|
||||
group_by: "owner_id",
|
||||
count: true,
|
||||
sum: "amount",
|
||||
where: { stage: "closed_won" },
|
||||
order_by: "sum_amount DESC",
|
||||
limit: 10
|
||||
});
|
||||
|
||||
const byActivity = dbQuery(
|
||||
`SELECT owner_id, CAST(COUNT(*) AS TEXT) as activity_count
|
||||
FROM crm_activities WHERE activity_date >= date('now', '-30 days')
|
||||
GROUP BY owner_id ORDER BY activity_count DESC LIMIT 10`
|
||||
);
|
||||
const activityResult = dbGroupBy("crm_activities", {
|
||||
group_by: "owner_id",
|
||||
count: true,
|
||||
where: "activity_date >= date('now', '-30 days')",
|
||||
order_by: "cnt DESC",
|
||||
limit: 10
|
||||
});
|
||||
|
||||
const leaderboard = [];
|
||||
for (const row of (byDealValue || [])) {
|
||||
for (const row of (dealResult.data || [])) {
|
||||
leaderboard.push({
|
||||
owner_id: row.owner_id,
|
||||
won_deals: parseInt(row.deal_count, 10),
|
||||
won_value: parseFloat(row.total_value),
|
||||
won_deals: parseInt(row.cnt, 10),
|
||||
won_value: parseFloat(row.sum_amount),
|
||||
});
|
||||
}
|
||||
|
||||
const activityLeaderboard = [];
|
||||
for (const row of (byActivity || [])) {
|
||||
for (const row of (activityResult.data || [])) {
|
||||
activityLeaderboard.push({
|
||||
owner_id: row.owner_id,
|
||||
activities: parseInt(row.activity_count, 10),
|
||||
activities: parseInt(row.cnt, 10),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -401,10 +347,9 @@ export function convertLead(input) {
|
||||
if (!userId) return fail(400, "user_id required");
|
||||
if (!contactId) return fail(400, "contact id required");
|
||||
|
||||
const contacts = dbQuery("SELECT id, lifecycle_stage, company_id FROM crm_contacts WHERE id = ?", [contactId]);
|
||||
if (!contacts || contacts.length === 0) return fail(404, "contact not found");
|
||||
const contact = dbFetchOne("crm_contacts", { id: contactId });
|
||||
if (!contact) return fail(404, "contact not found");
|
||||
|
||||
const contact = contacts[0];
|
||||
const currentStage = contact.lifecycle_stage;
|
||||
|
||||
const stageOrder = ["subscriber", "lead", "marketing_qualified_lead", "sales_qualified_lead", "opportunity", "customer"];
|
||||
@@ -417,17 +362,11 @@ export function convertLead(input) {
|
||||
if (targetIdx <= currentIdx) return fail(400, `cannot convert backward from ${currentStage} to ${targetStage}`);
|
||||
|
||||
const now = nowISO();
|
||||
const r = dbExec(
|
||||
"UPDATE crm_contacts SET lifecycle_stage = ?, updated_at = ? WHERE id = ?",
|
||||
[targetStage, now, contactId]
|
||||
);
|
||||
const r = dbUpdate("crm_contacts", { lifecycle_stage: targetStage, updated_at: now }, { id: contactId });
|
||||
if (r.error) return fail(500, r.error);
|
||||
|
||||
const activityId = newId();
|
||||
dbExec(
|
||||
"INSERT INTO crm_activities (id, tenant_id, type, subject, content, contact_id, company_id, owner_id, activity_date, created_at, updated_at) VALUES (?, 'default', 'note', ?, ?, ?, ?, ?, ?, ?, ?)",
|
||||
[activityId, `Lifecycle: ${currentStage} → ${targetStage}`, JSON.stringify({ from: currentStage, to: targetStage }), contactId, contact.company_id || null, userId, now, now, now]
|
||||
);
|
||||
dbInsert("crm_activities", { id: activityId, tenant_id: "default", type: "note", subject: `Lifecycle: ${currentStage} → ${targetStage}`, content: JSON.stringify({ from: currentStage, to: targetStage }), contact_id: contactId, company_id: contact.company_id || null, owner_id: userId, activity_date: now, created_at: now, updated_at: now });
|
||||
|
||||
eventEmit("crm.contact_converted", JSON.stringify({
|
||||
contact_id: contactId,
|
||||
@@ -459,13 +398,8 @@ export function getFunnelReport() {
|
||||
const funnel = [];
|
||||
let prevCount = null;
|
||||
for (const stage of stages) {
|
||||
const result = dbQuery(
|
||||
`SELECT CAST(COUNT(*) AS TEXT) as cnt, CAST(COALESCE(SUM(amount), 0) AS TEXT) as total
|
||||
FROM crm_deals WHERE stage = ?`,
|
||||
[stage]
|
||||
);
|
||||
const count = result?.[0] ? parseInt(result[0].cnt, 10) : 0;
|
||||
const total = result?.[0] ? parseFloat(result[0].total) : 0;
|
||||
const count = dbCount("crm_deals", { stage });
|
||||
const total = dbSum("crm_deals", "amount", { stage });
|
||||
|
||||
const conversionRate = prevCount != null && prevCount > 0
|
||||
? Math.round((count / prevCount) * 100)
|
||||
@@ -498,19 +432,24 @@ export function getFunnelReport() {
|
||||
// ── GET /reports/activities ──────────────────────────────────
|
||||
|
||||
export function getActivityReport() {
|
||||
const byType = dbQuery(
|
||||
`SELECT type, CAST(COUNT(*) AS TEXT) as cnt FROM crm_activities GROUP BY type ORDER BY cnt DESC`
|
||||
);
|
||||
const typeResult = dbGroupBy("crm_activities", {
|
||||
group_by: "type",
|
||||
count: true,
|
||||
order_by: "cnt DESC"
|
||||
});
|
||||
const typeBreakdown = {};
|
||||
for (const row of (byType || [])) {
|
||||
for (const row of (typeResult.data || [])) {
|
||||
typeBreakdown[row.type] = parseInt(row.cnt, 10);
|
||||
}
|
||||
|
||||
const byOwner = dbQuery(
|
||||
`SELECT owner_id, CAST(COUNT(*) AS TEXT) as cnt FROM crm_activities GROUP BY owner_id ORDER BY cnt DESC LIMIT 10`
|
||||
);
|
||||
const ownerResult = dbGroupBy("crm_activities", {
|
||||
group_by: "owner_id",
|
||||
count: true,
|
||||
order_by: "cnt DESC",
|
||||
limit: 10
|
||||
});
|
||||
const ownerBreakdown = [];
|
||||
for (const row of (byOwner || [])) {
|
||||
for (const row of (ownerResult.data || [])) {
|
||||
ownerBreakdown.push({ owner_id: row.owner_id, count: parseInt(row.cnt, 10) });
|
||||
}
|
||||
|
||||
|
||||
Vendored
+20
@@ -2,6 +2,17 @@ export interface DbExecResult {
|
||||
error?: string;
|
||||
rows_affected?: number;
|
||||
}
|
||||
export interface DbCrudResult {
|
||||
rows_affected?: number;
|
||||
}
|
||||
export interface DbFetchAllResult {
|
||||
data: Record<string, unknown>[];
|
||||
total: number;
|
||||
}
|
||||
export interface DbGroupByResult {
|
||||
data: Record<string, unknown>[];
|
||||
total: number;
|
||||
}
|
||||
export interface PluginError {
|
||||
__plugin_error: boolean;
|
||||
__status: number;
|
||||
@@ -10,6 +21,15 @@ export interface PluginError {
|
||||
export declare const SDK_VERSION: string;
|
||||
export declare function dbQuery(sql: string, params?: unknown[]): Record<string, unknown>[];
|
||||
export declare function dbExec(sql: string, params?: unknown[]): DbExecResult;
|
||||
export declare function dbInsert(table: string, data: Record<string, unknown> | string, options?: Record<string, unknown>): DbCrudResult;
|
||||
export declare function dbFetchOne(table: string, where: Record<string, unknown> | string | unknown[], options?: Record<string, unknown>): Record<string, unknown> | null;
|
||||
export declare function dbFetchAll(table: string, where?: Record<string, unknown> | string | unknown[] | null, options?: Record<string, unknown>): DbFetchAllResult;
|
||||
export declare function dbUpdate(table: string, data: Record<string, unknown> | string, where: Record<string, unknown> | string | unknown[], options?: Record<string, unknown>): DbCrudResult;
|
||||
export declare function dbDelete(table: string, where: Record<string, unknown> | string | unknown[], options?: Record<string, unknown>): DbCrudResult;
|
||||
export declare function dbCount(table: string, where?: Record<string, unknown> | string | unknown[] | null, options?: Record<string, unknown>): number;
|
||||
export declare function dbIncrement(table: string, columns: Record<string, number> | string, where?: Record<string, unknown> | string | unknown[] | null, options?: Record<string, unknown>): DbCrudResult;
|
||||
export declare function dbSum(table: string, column: string, where?: Record<string, unknown> | string | unknown[] | null, options?: Record<string, unknown>): number;
|
||||
export declare function dbGroupBy(table: string, options: Record<string, unknown> | string): DbGroupByResult;
|
||||
export declare function dbBegin(): {
|
||||
ok: boolean;
|
||||
};
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"module": "es2024",
|
||||
"moduleResolution": "node",
|
||||
"checkJs": true,
|
||||
"noEmit": true,
|
||||
"strict": false,
|
||||
"paths": {
|
||||
"sdk": ["./sdk.d.ts"]
|
||||
}
|
||||
},
|
||||
"include": ["*.js", "*.d.ts"]
|
||||
}
|
||||
@@ -1,200 +0,0 @@
|
||||
import { dbQuery, dbExec, ok, fail, extractJson, newId, dbBegin, dbCommit, dbRollback } from 'sdk';
|
||||
|
||||
const nowISO = () => new Date().toISOString();
|
||||
|
||||
// ── GET /products ────────────────────────────────────────────
|
||||
|
||||
export function listProducts() {
|
||||
const rows = dbQuery("SELECT id, name, slug, price, compare_at_price, stock, sku, images, featured FROM products WHERE status = 'published' ORDER BY created_at DESC LIMIT 50");
|
||||
if (!rows) return fail(500, "query failed");
|
||||
return ok({ items: rows, total: rows.length });
|
||||
}
|
||||
|
||||
// ── GET /products/:id ────────────────────────────────────────
|
||||
|
||||
export function getProduct(input) {
|
||||
const id = extractJson(input, "params.id");
|
||||
const rows = dbQuery("SELECT id, name, slug, description, price, compare_at_price, stock, sku, images, featured, weight FROM products WHERE id = ? AND status = 'published'", [id]);
|
||||
if (!rows) return fail(500, "query failed");
|
||||
if (rows.length === 0) return fail(404, "product not found");
|
||||
return ok(rows[0]);
|
||||
}
|
||||
|
||||
// ── GET /cart ────────────────────────────────────────────────
|
||||
|
||||
export function viewCart(input) {
|
||||
const data = extractJson(input, "body");
|
||||
const userId = data.user_id;
|
||||
if (!userId) return fail(400, "user_id required");
|
||||
const rows = dbQuery("SELECT c.id as cart_id, c.product_id, c.quantity, p.name, p.price, p.stock, p.sku FROM cart_items c LEFT JOIN products p ON c.product_id = p.id WHERE c.user_id = ?", [userId]);
|
||||
if (!rows) return fail(500, "query failed");
|
||||
let total = 0;
|
||||
for (const row of rows) {
|
||||
row.subtotal = (row.price || 0) * (row.quantity || 0);
|
||||
total += row.subtotal;
|
||||
}
|
||||
return ok({ items: rows, total });
|
||||
}
|
||||
|
||||
// ── POST /cart ───────────────────────────────────────────────
|
||||
|
||||
export function addToCart(input) {
|
||||
const data = extractJson(input, "body");
|
||||
const userId = data.user_id;
|
||||
const productId = data.product_id;
|
||||
const quantity = data.quantity || 1;
|
||||
if (!userId || !productId) return fail(400, "user_id and product_id required");
|
||||
|
||||
const products = dbQuery("SELECT id, name, price, stock, status FROM products WHERE id = ?", [productId]);
|
||||
if (!products) return fail(500, "query failed");
|
||||
if (products.length === 0) return fail(404, "product not found");
|
||||
if (products[0].status !== "published") return fail(400, "product not available");
|
||||
if (products[0].stock < quantity) return fail(400, "insufficient stock");
|
||||
|
||||
const existing = dbQuery("SELECT id, quantity FROM cart_items WHERE user_id = ? AND product_id = ?", [userId, productId]);
|
||||
if (!existing) return fail(500, "query failed");
|
||||
|
||||
if (existing.length > 0) {
|
||||
const newQty = existing[0].quantity + quantity;
|
||||
const r = dbExec("UPDATE cart_items SET quantity = ? WHERE id = ?", [newQty, existing[0].id]);
|
||||
if (r.error) return fail(500, r.error);
|
||||
} else {
|
||||
const id = newId();
|
||||
const now = nowISO();
|
||||
const r = dbExec("INSERT INTO cart_items (id, tenant_id, user_id, product_id, quantity, created_at, updated_at) VALUES (?, 'default', ?, ?, ?, ?, ?)", [id, userId, productId, quantity, now, now]);
|
||||
if (r.error) return fail(500, r.error);
|
||||
}
|
||||
return ok({ added: true });
|
||||
}
|
||||
|
||||
// ── DELETE /cart ─────────────────────────────────────────────
|
||||
|
||||
export function clearCart(input) {
|
||||
const data = extractJson(input, "body");
|
||||
if (!data.user_id) return fail(400, "user_id required");
|
||||
const r = dbExec("DELETE FROM cart_items WHERE user_id = ?", [data.user_id]);
|
||||
if (r.error) return fail(500, r.error);
|
||||
return ok({ cleared: true, rows_affected: r.rows_affected });
|
||||
}
|
||||
|
||||
// ── PUT /cart/:id ────────────────────────────────────────────
|
||||
|
||||
export function updateCartItem(input) {
|
||||
const itemId = extractJson(input, "params.id");
|
||||
const data = extractJson(input, "body");
|
||||
if (!itemId) return fail(400, "item id required");
|
||||
if (!data.quantity || data.quantity < 1) return fail(400, "quantity must be >= 1");
|
||||
const r = dbExec("UPDATE cart_items SET quantity = ? WHERE id = ?", [data.quantity, itemId]);
|
||||
if (r.error) return fail(500, r.error);
|
||||
if (r.rows_affected === 0) return fail(404, "cart item not found");
|
||||
return ok({ updated: true });
|
||||
}
|
||||
|
||||
// ── DELETE /cart/:id ─────────────────────────────────────────
|
||||
|
||||
export function removeCartItem(input) {
|
||||
const itemId = extractJson(input, "params.id");
|
||||
if (!itemId) return fail(400, "item id required");
|
||||
const r = dbExec("DELETE FROM cart_items WHERE id = ?", [itemId]);
|
||||
if (r.error) return fail(500, r.error);
|
||||
if (r.rows_affected === 0) return fail(404, "cart item not found");
|
||||
return ok({ removed: true });
|
||||
}
|
||||
|
||||
// ── POST /checkout (事务) ────────────────────────────────────
|
||||
|
||||
export function checkout(input) {
|
||||
const data = extractJson(input, "body");
|
||||
const userId = data.user_id;
|
||||
if (!userId) return fail(400, "user_id required");
|
||||
|
||||
const cartItems = dbQuery("SELECT c.id as cart_id, c.product_id, c.quantity, p.name, p.price, p.stock, p.status FROM cart_items c LEFT JOIN products p ON c.product_id = p.id WHERE c.user_id = ?", [userId]);
|
||||
if (!cartItems) return fail(500, "query failed");
|
||||
if (cartItems.length === 0) return fail(400, "cart is empty");
|
||||
|
||||
for (const item of cartItems) {
|
||||
if (!item.name) return fail(400, `product ${item.product_id} not found`);
|
||||
if (item.status !== "published") return fail(400, `product ${item.name} not available`);
|
||||
if (item.stock < item.quantity) return fail(400, `insufficient stock for ${item.name}`);
|
||||
}
|
||||
|
||||
let totalAmount = 0;
|
||||
const orderItems = [];
|
||||
for (const item of cartItems) {
|
||||
const subtotal = item.price * item.quantity;
|
||||
totalAmount += subtotal;
|
||||
orderItems.push({ product_id: item.product_id, product_name: item.name, price: item.price, quantity: item.quantity, subtotal });
|
||||
}
|
||||
|
||||
dbBegin();
|
||||
|
||||
const orderId = newId();
|
||||
const orderNo = `ORD-${Date.now().toString(36).toUpperCase()}-${Math.random().toString(36).substring(2, 6).toUpperCase()}`;
|
||||
const shippingJson = data.shipping_address ? JSON.stringify(data.shipping_address) : "";
|
||||
|
||||
const r = dbExec(
|
||||
"INSERT INTO orders (id, tenant_id, order_no, user_id, status, total_amount, shipping_address, note, created_at, updated_at) VALUES (?, 'default', ?, ?, 'pending', ?, ?, ?, ?, ?)",
|
||||
[orderId, orderNo, userId, totalAmount, shippingJson, data.note || "", nowISO(), nowISO()]
|
||||
);
|
||||
if (r.error) {
|
||||
dbRollback();
|
||||
return fail(500, `create order failed: ${r.error}`);
|
||||
}
|
||||
|
||||
for (const oi of orderItems) {
|
||||
const oiId = newId();
|
||||
const r2 = dbExec(
|
||||
"INSERT INTO order_items (id, order_id, product_id, product_name, price, quantity, subtotal) VALUES (?, ?, ?, ?, ?, ?, ?)",
|
||||
[oiId, orderId, oi.product_id, oi.product_name, oi.price, oi.quantity, oi.subtotal]
|
||||
);
|
||||
if (r2.error) {
|
||||
dbRollback();
|
||||
return fail(500, `create order item failed: ${r2.error}`);
|
||||
}
|
||||
|
||||
const r3 = dbExec("UPDATE products SET stock = stock - ? WHERE id = ? AND stock >= ?", [oi.quantity, oi.product_id, oi.quantity]);
|
||||
if (r3.error || r3.rows_affected === 0) {
|
||||
dbRollback();
|
||||
return fail(500, `stock deduction failed for ${oi.product_name}`);
|
||||
}
|
||||
}
|
||||
|
||||
dbExec("DELETE FROM cart_items WHERE user_id = ?", [userId]);
|
||||
|
||||
dbCommit();
|
||||
|
||||
return ok({
|
||||
order_id: orderId,
|
||||
order_no: orderNo,
|
||||
status: "pending",
|
||||
total_amount: totalAmount,
|
||||
items_count: orderItems.length,
|
||||
});
|
||||
}
|
||||
|
||||
// ── GET /orders ──────────────────────────────────────────────
|
||||
|
||||
export function listOrders(input) {
|
||||
const data = extractJson(input, "body");
|
||||
if (!data.user_id) return fail(400, "user_id required");
|
||||
const rows = dbQuery("SELECT id, order_no, status, total_amount, note, created_at FROM orders WHERE user_id = ? ORDER BY created_at DESC LIMIT 50", [data.user_id]);
|
||||
if (!rows) return fail(500, "query failed");
|
||||
return ok({ items: rows, total: rows.length });
|
||||
}
|
||||
|
||||
// ── GET /orders/:id ──────────────────────────────────────────
|
||||
|
||||
export function getOrder(input) {
|
||||
const orderId = extractJson(input, "params.id");
|
||||
const data = extractJson(input, "body");
|
||||
if (!orderId) return fail(400, "order id required");
|
||||
const orders = dbQuery("SELECT id, order_no, user_id, status, total_amount, shipping_address, note, paid_at, shipped_at, created_at FROM orders WHERE id = ?", [orderId]);
|
||||
if (!orders) return fail(500, "query failed");
|
||||
if (orders.length === 0) return fail(404, "order not found");
|
||||
const order = orders[0];
|
||||
if (data.user_id && order.user_id !== data.user_id) return fail(403, "forbidden");
|
||||
|
||||
const items = dbQuery("SELECT id, product_id, product_name, price, quantity, subtotal FROM order_items WHERE order_id = ?", [orderId]);
|
||||
order.items = items || [];
|
||||
return ok(order);
|
||||
}
|
||||
@@ -1,79 +0,0 @@
|
||||
[plugin]
|
||||
id = "com.raisfast.ecommerce"
|
||||
name = "E-Commerce API"
|
||||
version = "0.1.0"
|
||||
description = "电商 API:商品浏览、购物车、下单结算"
|
||||
author = "raisfast"
|
||||
license = "MIT"
|
||||
runtime = "js"
|
||||
language = "js"
|
||||
entry = "main.js"
|
||||
|
||||
[permissions]
|
||||
max_memory_mb = 16
|
||||
timeout_ms = 5000
|
||||
database = [
|
||||
"products",
|
||||
"read:product_categories",
|
||||
"orders",
|
||||
"order_items",
|
||||
"cart_items",
|
||||
]
|
||||
|
||||
[[routes]]
|
||||
method = "GET"
|
||||
path = "/api/v1/plugins/ecommerce/products"
|
||||
handler = "listProducts"
|
||||
|
||||
[[routes]]
|
||||
method = "GET"
|
||||
path = "/api/v1/plugins/ecommerce/products/:id"
|
||||
handler = "getProduct"
|
||||
|
||||
[[routes]]
|
||||
method = "GET"
|
||||
path = "/api/v1/plugins/ecommerce/cart"
|
||||
handler = "viewCart"
|
||||
auth = "member"
|
||||
|
||||
[[routes]]
|
||||
method = "POST"
|
||||
path = "/api/v1/plugins/ecommerce/cart"
|
||||
handler = "addToCart"
|
||||
auth = "member"
|
||||
|
||||
[[routes]]
|
||||
method = "DELETE"
|
||||
path = "/api/v1/plugins/ecommerce/cart"
|
||||
handler = "clearCart"
|
||||
auth = "member"
|
||||
|
||||
[[routes]]
|
||||
method = "PUT"
|
||||
path = "/api/v1/plugins/ecommerce/cart/:id"
|
||||
handler = "updateCartItem"
|
||||
auth = "member"
|
||||
|
||||
[[routes]]
|
||||
method = "DELETE"
|
||||
path = "/api/v1/plugins/ecommerce/cart/:id"
|
||||
handler = "removeCartItem"
|
||||
auth = "member"
|
||||
|
||||
[[routes]]
|
||||
method = "POST"
|
||||
path = "/api/v1/plugins/ecommerce/checkout"
|
||||
handler = "checkout"
|
||||
auth = "member"
|
||||
|
||||
[[routes]]
|
||||
method = "GET"
|
||||
path = "/api/v1/plugins/ecommerce/orders"
|
||||
handler = "listOrders"
|
||||
auth = "member"
|
||||
|
||||
[[routes]]
|
||||
method = "GET"
|
||||
path = "/api/v1/plugins/ecommerce/orders/:id"
|
||||
handler = "getOrder"
|
||||
auth = "member"
|
||||
-43
@@ -1,43 +0,0 @@
|
||||
export interface DbExecResult {
|
||||
error?: string;
|
||||
rows_affected?: number;
|
||||
}
|
||||
export interface PluginError {
|
||||
__plugin_error: boolean;
|
||||
__status: number;
|
||||
__message: string;
|
||||
}
|
||||
export declare const SDK_VERSION: string;
|
||||
export declare function dbQuery(sql: string, params?: unknown[]): Record<string, unknown>[];
|
||||
export declare function dbExec(sql: string, params?: unknown[]): DbExecResult;
|
||||
export declare function dbBegin(): {
|
||||
ok: boolean;
|
||||
};
|
||||
export declare function dbCommit(): {
|
||||
ok: boolean;
|
||||
};
|
||||
export declare function dbRollback(): {
|
||||
ok: boolean;
|
||||
};
|
||||
export declare function httpGet(url: string): string | null;
|
||||
export declare function httpGetJson(url: string): Record<string, unknown> | null;
|
||||
export declare function httpPost(url: string, body: Record<string, unknown> | string): string | null;
|
||||
export declare function httpPostJson(url: string, body: Record<string, unknown> | string): Record<string, unknown> | null;
|
||||
export declare function configGet(key: string): string | null;
|
||||
export declare function storeGet(key: string): string | null;
|
||||
export declare function storeSet(key: string, value: string): boolean;
|
||||
export declare function vfsRead(path: string): string | null;
|
||||
export declare function vfsWrite(path: string, content: string): boolean;
|
||||
export declare function vfsDelete(path: string): boolean;
|
||||
export declare function vfsExists(path: string): boolean;
|
||||
export declare function vfsList(path: string): string[] | null;
|
||||
export declare function vfsStat(path: string): Record<string, unknown> | null;
|
||||
export declare function getPost(slug: string): Record<string, unknown> | null;
|
||||
export declare function ok(data: unknown): any;
|
||||
export declare function fail(status: number, msg: string): PluginError;
|
||||
export declare function extractJson(input: any, field?: string): any;
|
||||
export declare function logInfo(msg: string): void;
|
||||
export declare function logWarn(msg: string): void;
|
||||
export declare function logError(msg: string): void;
|
||||
export declare function newId(): string;
|
||||
export declare function eventEmit(type: string, data: string | Record<string, unknown>): void;
|
||||
@@ -8,17 +8,10 @@ local sdk = require("sdk")
|
||||
Plugin = {}
|
||||
|
||||
Plugin.stats_overview = function(input)
|
||||
local posts = sdk.dbQuery("SELECT CAST(COUNT(*) AS TEXT) as total FROM posts")
|
||||
if not posts then return sdk.fail(500, "query failed") end
|
||||
|
||||
local comments = sdk.dbQuery("SELECT CAST(COUNT(*) AS TEXT) as total FROM comments")
|
||||
local users = sdk.dbQuery("SELECT CAST(COUNT(*) AS TEXT) as total FROM users")
|
||||
local published = sdk.dbQuery("SELECT CAST(COUNT(*) AS TEXT) as total FROM posts WHERE status = 'published'")
|
||||
|
||||
local total_posts = posts[1] and tonumber(posts[1].total) or 0
|
||||
local total_comments = comments and comments[1] and tonumber(comments[1].total) or 0
|
||||
local total_users = users and users[1] and tonumber(users[1].total) or 0
|
||||
local total_published = published and published[1] and tonumber(published[1].total) or 0
|
||||
local total_posts = sdk.dbCount("posts")
|
||||
local total_comments = sdk.dbCount("comments")
|
||||
local total_users = sdk.dbCount("users")
|
||||
local total_published = sdk.dbCount("posts", { status = "published" })
|
||||
|
||||
sdk.logInfo("[blog-stats] GET overview")
|
||||
|
||||
@@ -31,27 +24,19 @@ Plugin.stats_overview = function(input)
|
||||
end
|
||||
|
||||
Plugin.stats_posts = function(input)
|
||||
local result = sdk.dbQuery("SELECT status, CAST(COUNT(*) AS TEXT) as count FROM posts GROUP BY status")
|
||||
if not result then return sdk.fail(500, "query failed") end
|
||||
return sdk.ok(result)
|
||||
local result = sdk.dbGroupBy("posts", {
|
||||
group_by = "status",
|
||||
count = true
|
||||
})
|
||||
return sdk.ok(result.data or {})
|
||||
end
|
||||
|
||||
Plugin.stats_recent = function(input)
|
||||
local result = sdk.dbQuery(
|
||||
"SELECT title, slug, view_count, published_at "
|
||||
.. "FROM posts WHERE status = 'published' "
|
||||
.. "ORDER BY published_at DESC LIMIT 10"
|
||||
)
|
||||
if not result then return sdk.fail(500, "query failed") end
|
||||
return sdk.ok(result)
|
||||
local result = sdk.dbFetchAll("posts", { status = "published" }, { order_by = "published_at DESC", limit = 10 })
|
||||
return sdk.ok(result.data or {})
|
||||
end
|
||||
|
||||
Plugin.stats_top = function(input)
|
||||
local result = sdk.dbQuery(
|
||||
"SELECT title, slug, view_count "
|
||||
.. "FROM posts WHERE status = 'published' "
|
||||
.. "ORDER BY view_count DESC LIMIT 10"
|
||||
)
|
||||
if not result then return sdk.fail(500, "query failed") end
|
||||
return sdk.ok(result)
|
||||
local result = sdk.dbFetchAll("posts", { status = "published" }, { order_by = "view_count DESC", limit = 10 })
|
||||
return sdk.ok(result.data or {})
|
||||
end
|
||||
|
||||
+20
@@ -2,6 +2,17 @@ export interface DbExecResult {
|
||||
error?: string;
|
||||
rows_affected?: number;
|
||||
}
|
||||
export interface DbCrudResult {
|
||||
rows_affected?: number;
|
||||
}
|
||||
export interface DbFetchAllResult {
|
||||
data: Record<string, unknown>[];
|
||||
total: number;
|
||||
}
|
||||
export interface DbGroupByResult {
|
||||
data: Record<string, unknown>[];
|
||||
total: number;
|
||||
}
|
||||
export interface PluginError {
|
||||
__plugin_error: boolean;
|
||||
__status: number;
|
||||
@@ -10,6 +21,15 @@ export interface PluginError {
|
||||
export declare const SDK_VERSION: string;
|
||||
export declare function dbQuery(sql: string, params?: unknown[]): Record<string, unknown>[];
|
||||
export declare function dbExec(sql: string, params?: unknown[]): DbExecResult;
|
||||
export declare function dbInsert(table: string, data: Record<string, unknown> | string, options?: Record<string, unknown>): DbCrudResult;
|
||||
export declare function dbFetchOne(table: string, where: Record<string, unknown> | string | unknown[], options?: Record<string, unknown>): Record<string, unknown> | null;
|
||||
export declare function dbFetchAll(table: string, where?: Record<string, unknown> | string | unknown[] | null, options?: Record<string, unknown>): DbFetchAllResult;
|
||||
export declare function dbUpdate(table: string, data: Record<string, unknown> | string, where: Record<string, unknown> | string | unknown[], options?: Record<string, unknown>): DbCrudResult;
|
||||
export declare function dbDelete(table: string, where: Record<string, unknown> | string | unknown[], options?: Record<string, unknown>): DbCrudResult;
|
||||
export declare function dbCount(table: string, where?: Record<string, unknown> | string | unknown[] | null, options?: Record<string, unknown>): number;
|
||||
export declare function dbIncrement(table: string, columns: Record<string, number> | string, where?: Record<string, unknown> | string | unknown[] | null, options?: Record<string, unknown>): DbCrudResult;
|
||||
export declare function dbSum(table: string, column: string, where?: Record<string, unknown> | string | unknown[] | null, options?: Record<string, unknown>): number;
|
||||
export declare function dbGroupBy(table: string, options: Record<string, unknown> | string): DbGroupByResult;
|
||||
export declare function dbBegin(): {
|
||||
ok: boolean;
|
||||
};
|
||||
|
||||
+100
-104
@@ -1,4 +1,6 @@
|
||||
import { dbQuery, dbExec, ok, fail, extractJson, logInfo, logWarn, logError, newId, eventEmit } from 'sdk';
|
||||
import { dbQuery, dbExec, ok, fail, extractJson, logInfo, logWarn, logError, newId, eventEmit,
|
||||
dbInsert, dbFetchOne, dbFetchAll, dbUpdate, dbDelete, dbCount,
|
||||
dbIncrement, dbSum } from 'sdk';
|
||||
|
||||
const nowISO = () => new Date().toISOString();
|
||||
|
||||
@@ -12,8 +14,8 @@ export function on_content_creating(input) {
|
||||
if (ct === "forum_reply") {
|
||||
const topicId = body.topic_id;
|
||||
if (topicId) {
|
||||
const topics = dbQuery("SELECT is_locked FROM forum_topics WHERE id = ?", [topicId]);
|
||||
if (topics?.length > 0 && topics[0].is_locked) {
|
||||
const topic = dbFetchOne("forum_topics", { id: topicId });
|
||||
if (topic && topic.is_locked) {
|
||||
return fail(400, "topic is locked");
|
||||
}
|
||||
}
|
||||
@@ -28,26 +30,31 @@ export function on_content_created(input) {
|
||||
const id = data.id;
|
||||
|
||||
if (ct === "forum_topic") {
|
||||
const topics = dbQuery("SELECT board_id FROM forum_topics WHERE id = ?", [id]);
|
||||
if (topics?.length > 0) {
|
||||
const topic = dbFetchOne("forum_topics", { id });
|
||||
if (topic) {
|
||||
const now = nowISO();
|
||||
dbExec("UPDATE forum_boards SET topic_count = topic_count + 1, post_count = post_count + 1, last_activity_at = ?, last_topic_id = ? WHERE id = ?",
|
||||
[now, id, topics[0].board_id]);
|
||||
dbIncrement("forum_boards",
|
||||
{ topic_count: 1, post_count: 1 },
|
||||
{ id: topic.board_id },
|
||||
{ set: { last_activity_at: now, last_topic_id: id } });
|
||||
}
|
||||
}
|
||||
|
||||
if (ct === "forum_reply") {
|
||||
const replies = dbQuery("SELECT topic_id, author_id FROM forum_replies WHERE id = ?", [id]);
|
||||
if (replies?.length > 0) {
|
||||
const reply = replies[0];
|
||||
const reply = dbFetchOne("forum_replies", { id });
|
||||
if (reply) {
|
||||
const now = nowISO();
|
||||
dbExec("UPDATE forum_topics SET reply_count = reply_count + 1, last_reply_at = ?, last_reply_user_id = ?, updated_at = ? WHERE id = ?",
|
||||
[now, reply.author_id, now, reply.topic_id]);
|
||||
dbIncrement("forum_topics",
|
||||
{ reply_count: 1 },
|
||||
{ id: reply.topic_id },
|
||||
{ set: { last_reply_at: now, last_reply_user_id: reply.author_id, updated_at: now } });
|
||||
|
||||
const topics = dbQuery("SELECT board_id FROM forum_topics WHERE id = ?", [reply.topic_id]);
|
||||
if (topics?.length > 0) {
|
||||
dbExec("UPDATE forum_boards SET post_count = post_count + 1, last_activity_at = ? WHERE id = ?",
|
||||
[now, topics[0].board_id]);
|
||||
const topic = dbFetchOne("forum_topics", { id: reply.topic_id });
|
||||
if (topic) {
|
||||
dbIncrement("forum_boards",
|
||||
{ post_count: 1 },
|
||||
{ id: topic.board_id },
|
||||
{ set: { last_activity_at: now } });
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -61,18 +68,22 @@ export function on_content_deleted(input) {
|
||||
const id = data.id;
|
||||
|
||||
if (ct === "forum_topic") {
|
||||
const topics = dbQuery("SELECT board_id FROM forum_topics WHERE id = ?", [id]);
|
||||
if (topics?.length > 0) {
|
||||
dbExec("UPDATE forum_boards SET topic_count = CASE WHEN topic_count > 0 THEN topic_count - 1 ELSE 0 END, post_count = CASE WHEN post_count > 0 THEN post_count - 1 ELSE 0 END WHERE id = ?",
|
||||
[topics[0].board_id]);
|
||||
const topic = dbFetchOne("forum_topics", { id });
|
||||
if (topic) {
|
||||
dbIncrement("forum_boards",
|
||||
{ topic_count: -1, post_count: -1 },
|
||||
{ id: topic.board_id },
|
||||
{ min: 0 });
|
||||
}
|
||||
}
|
||||
|
||||
if (ct === "forum_reply") {
|
||||
const replies = dbQuery("SELECT topic_id FROM forum_replies WHERE id = ?", [id]);
|
||||
if (replies?.length > 0) {
|
||||
dbExec("UPDATE forum_topics SET reply_count = CASE WHEN reply_count > 0 THEN reply_count - 1 ELSE 0 END WHERE id = ?",
|
||||
[replies[0].topic_id]);
|
||||
const reply = dbFetchOne("forum_replies", { id });
|
||||
if (reply) {
|
||||
dbIncrement("forum_topics",
|
||||
{ reply_count: -1 },
|
||||
{ id: reply.topic_id },
|
||||
{ min: 0 });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,7 +96,7 @@ export function on_content_viewed(input) {
|
||||
const id = data.id;
|
||||
|
||||
if (ct === "forum_topic") {
|
||||
dbExec("UPDATE forum_topics SET view_count = view_count + 1 WHERE id = ?", [id]);
|
||||
dbIncrement("forum_topics", { view_count: 1 }, { id });
|
||||
}
|
||||
|
||||
return ok(data);
|
||||
@@ -101,23 +112,16 @@ export function listBoardTopics(input) {
|
||||
if (pageSize < 1 || pageSize > 100) pageSize = 20;
|
||||
const offset = (page - 1) * pageSize;
|
||||
|
||||
const boards = dbQuery("SELECT id FROM forum_boards WHERE slug = ?", [slug]);
|
||||
if (!boards || boards.length === 0) return fail(404, `board not found for slug: ${slug}`);
|
||||
const boardId = boards[0].id;
|
||||
const board = dbFetchOne("forum_boards", { slug });
|
||||
if (!board) return fail(404, `board not found for slug: ${slug}`);
|
||||
const boardId = board.id;
|
||||
|
||||
const totalResult = dbQuery("SELECT CAST(COUNT(*) AS TEXT) as cnt FROM forum_topics WHERE board_id = ?", [boardId]);
|
||||
const total = totalResult?.[0] ? parseInt(totalResult[0].cnt, 10) : 0;
|
||||
const total = dbCount("forum_topics", { board_id: boardId });
|
||||
|
||||
const rows = dbQuery(
|
||||
"SELECT id, title, slug, author_id, reply_count, view_count, is_pinned, is_locked, is_solved, " +
|
||||
"last_reply_at, last_reply_user_id, tags, created_at " +
|
||||
"FROM forum_topics WHERE board_id = ? " +
|
||||
"ORDER BY is_pinned DESC, last_reply_at DESC, created_at DESC " +
|
||||
"LIMIT ? OFFSET ?",
|
||||
[boardId, pageSize, offset]
|
||||
);
|
||||
const result = dbFetchAll("forum_topics", { board_id: boardId },
|
||||
{ order_by: "is_pinned DESC, last_reply_at DESC, created_at DESC", limit: pageSize, offset });
|
||||
|
||||
return ok({ items: rows || [], total, page, page_size: pageSize, board_id: boardId });
|
||||
return ok({ items: result.data || [], total, page, page_size: pageSize, board_id: boardId });
|
||||
}
|
||||
|
||||
// ── PUT /replies/:id/accept ─────────────────────────────────
|
||||
@@ -128,18 +132,18 @@ export function acceptAnswer(input) {
|
||||
const userId = data.user_id;
|
||||
if (!userId) return fail(400, "user_id required");
|
||||
|
||||
const replies = dbQuery("SELECT id, topic_id, author_id FROM forum_replies WHERE id = ?", [replyId]);
|
||||
if (!replies || replies.length === 0) return fail(404, "reply not found");
|
||||
const reply = dbFetchOne("forum_replies", { id: replyId });
|
||||
if (!reply) return fail(404, "reply not found");
|
||||
|
||||
const reply = replies[0];
|
||||
const topics = dbQuery("SELECT id, author_id FROM forum_topics WHERE id = ?", [reply.topic_id]);
|
||||
if (!topics || topics.length === 0) return fail(404, "topic not found");
|
||||
const topic = dbFetchOne("forum_topics", { id: reply.topic_id });
|
||||
if (!topic) return fail(404, "topic not found");
|
||||
|
||||
if (topics[0].author_id !== userId) return fail(403, "only topic author can accept answer");
|
||||
if (topic.author_id !== userId) return fail(403, "only topic author can accept answer");
|
||||
|
||||
dbExec("UPDATE forum_replies SET is_answer = 0 WHERE topic_id = ? AND is_answer = 1", [reply.topic_id]);
|
||||
dbExec("UPDATE forum_replies SET is_answer = 1, updated_at = ? WHERE id = ?", [nowISO(), replyId]);
|
||||
dbExec("UPDATE forum_topics SET is_solved = 1, updated_at = ? WHERE id = ?", [nowISO(), reply.topic_id]);
|
||||
const now = nowISO();
|
||||
dbUpdate("forum_replies", { is_answer: 0 }, { topic_id: reply.topic_id, is_answer: 1 });
|
||||
dbUpdate("forum_replies", { is_answer: 1, updated_at: now }, { id: replyId });
|
||||
dbUpdate("forum_topics", { is_solved: 1, updated_at: now }, { id: reply.topic_id });
|
||||
|
||||
return ok({ id: replyId, is_answer: true, topic_id: reply.topic_id });
|
||||
}
|
||||
@@ -160,18 +164,17 @@ export function vote(input) {
|
||||
if (!targetId) return fail(400, "target_id required");
|
||||
if (targetType !== "topic" && targetType !== "reply") return fail(400, "target_type must be topic or reply");
|
||||
|
||||
const existing = dbQuery("SELECT id, value FROM forum_votes WHERE target_type = ? AND target_id = ? AND user_id = ?", [targetType, targetId, userId]);
|
||||
if (existing?.length > 0) {
|
||||
const oldValue = parseInt(existing[0].value, 10);
|
||||
const existing = dbFetchOne("forum_votes", { target_type: targetType, target_id: targetId, user_id: userId });
|
||||
if (existing) {
|
||||
const oldValue = parseInt(existing.value, 10);
|
||||
const diff = value - oldValue;
|
||||
if (diff === 0) return fail(400, "already voted");
|
||||
dbExec("UPDATE forum_votes SET value = ?, updated_at = ? WHERE id = ?", [value, nowISO(), existing[0].id]);
|
||||
dbUpdate("forum_votes", { value, updated_at: nowISO() }, { id: existing.id });
|
||||
updateVoteCount(targetType, targetId, diff);
|
||||
} else {
|
||||
const id = newId();
|
||||
const now = nowISO();
|
||||
dbExec("INSERT INTO forum_votes (id, tenant_id, target_type, target_id, user_id, value, created_at, updated_at) VALUES (?, 'default', ?, ?, ?, ?, ?, ?)",
|
||||
[id, targetType, targetId, userId, value, now, now]);
|
||||
dbInsert("forum_votes", { id, tenant_id: "default", target_type: targetType, target_id: targetId, user_id: userId, value, created_at: now, updated_at: now });
|
||||
updateVoteCount(targetType, targetId, value);
|
||||
}
|
||||
|
||||
@@ -190,11 +193,11 @@ export function unvote(input) {
|
||||
if (!targetType) return fail(400, "target_type required");
|
||||
if (!targetId) return fail(400, "target_id required");
|
||||
|
||||
const existing = dbQuery("SELECT id, value FROM forum_votes WHERE target_type = ? AND target_id = ? AND user_id = ?", [targetType, targetId, userId]);
|
||||
if (!existing || existing.length === 0) return fail(404, "vote not found");
|
||||
const existing = dbFetchOne("forum_votes", { target_type: targetType, target_id: targetId, user_id: userId });
|
||||
if (!existing) return fail(404, "vote not found");
|
||||
|
||||
const oldValue = parseInt(existing[0].value, 10);
|
||||
dbExec("DELETE FROM forum_votes WHERE id = ?", [existing[0].id]);
|
||||
const oldValue = parseInt(existing.value, 10);
|
||||
dbDelete("forum_votes", { id: existing.id });
|
||||
updateVoteCount(targetType, targetId, -oldValue);
|
||||
|
||||
return ok({ removed: true });
|
||||
@@ -202,7 +205,7 @@ export function unvote(input) {
|
||||
|
||||
const updateVoteCount = (targetType, targetId, diff) => {
|
||||
const table = targetType === "topic" ? "forum_topics" : "forum_replies";
|
||||
dbExec(`UPDATE ${table} SET vote_count = vote_count + ? WHERE id = ?`, [diff, targetId]);
|
||||
dbIncrement(table, { vote_count: diff }, { id: targetId });
|
||||
};
|
||||
|
||||
// ── Polls ───────────────────────────────────────────────────
|
||||
@@ -223,25 +226,23 @@ export function createPoll(input) {
|
||||
if (maxChoices < 1) maxChoices = 1;
|
||||
if (maxChoices > options.length) maxChoices = options.length;
|
||||
|
||||
const topics = dbQuery("SELECT id, author_id FROM forum_topics WHERE id = ?", [topicId]);
|
||||
if (!topics || topics.length === 0) return fail(404, "topic not found");
|
||||
if (topics[0].author_id !== userId) return fail(403, "only topic author can create poll");
|
||||
const topic = dbFetchOne("forum_topics", { id: topicId });
|
||||
if (!topic) return fail(404, "topic not found");
|
||||
if (topic.author_id !== userId) return fail(403, "only topic author can create poll");
|
||||
|
||||
const existing = dbQuery("SELECT id FROM forum_polls WHERE topic_id = ?", [topicId]);
|
||||
if (existing?.length > 0) return fail(400, "poll already exists for this topic");
|
||||
const existing = dbFetchOne("forum_polls", { topic_id: topicId });
|
||||
if (existing) return fail(400, "poll already exists for this topic");
|
||||
|
||||
const pollId = newId();
|
||||
const now = nowISO();
|
||||
dbExec("INSERT INTO forum_polls (id, tenant_id, topic_id, question, max_choices, is_closed, created_at, updated_at) VALUES (?, 'default', ?, ?, ?, 0, ?, ?)",
|
||||
[pollId, topicId, question, maxChoices, now, now]);
|
||||
dbInsert("forum_polls", { id: pollId, tenant_id: "default", topic_id: topicId, question, max_choices: maxChoices, is_closed: 0, created_at: now, updated_at: now });
|
||||
|
||||
const createdOptions = [];
|
||||
for (let i = 0; i < options.length; i++) {
|
||||
const optText = (options[i] || "").trim();
|
||||
if (!optText) continue;
|
||||
const optId = newId();
|
||||
dbExec("INSERT INTO forum_poll_options (id, tenant_id, poll_id, text, vote_count, sort_order) VALUES (?, 'default', ?, ?, 0, ?)",
|
||||
[optId, pollId, optText, i]);
|
||||
dbInsert("forum_poll_options", { id: optId, tenant_id: "default", poll_id: pollId, text: optText, vote_count: 0, sort_order: i });
|
||||
createdOptions.push({ id: optId, text: optText, vote_count: 0, sort_order: i });
|
||||
}
|
||||
|
||||
@@ -275,35 +276,32 @@ export function getPoll(input) {
|
||||
}
|
||||
}
|
||||
|
||||
const polls = dbQuery("SELECT id, topic_id, question, max_choices, is_closed, created_at FROM forum_polls WHERE topic_id = ?", [topicId]);
|
||||
if (!polls || polls.length === 0) return ok(null);
|
||||
const poll = dbFetchOne("forum_polls", { topic_id: topicId });
|
||||
if (!poll) return ok(null);
|
||||
|
||||
const poll = polls[0];
|
||||
const options = dbQuery("SELECT id, text, CAST(vote_count AS TEXT) as vote_count, CAST(sort_order AS TEXT) as sort_order FROM forum_poll_options WHERE poll_id = ? ORDER BY sort_order", [poll.id]);
|
||||
const optsResult = dbFetchAll("forum_poll_options", { poll_id: poll.id }, { order_by: "sort_order ASC" });
|
||||
const options = optsResult.data || [];
|
||||
|
||||
const totalResult = dbQuery("SELECT CAST(SUM(vote_count) AS TEXT) as total FROM forum_poll_options WHERE poll_id = ?", [poll.id]);
|
||||
const totalVotes = (totalResult?.[0]?.total) ? parseInt(totalResult[0].total, 10) : 0;
|
||||
const totalVotes = dbSum("forum_poll_options", "vote_count", { poll_id: poll.id });
|
||||
|
||||
const userVotes = [];
|
||||
if (userId) {
|
||||
const votes = dbQuery("SELECT option_id FROM forum_poll_votes WHERE poll_id = ? AND user_id = ?", [poll.id, userId]);
|
||||
if (votes) {
|
||||
for (const vote of votes) {
|
||||
userVotes.push(vote.option_id);
|
||||
const votesResult = dbFetchAll("forum_poll_votes", { poll_id: poll.id, user_id: userId });
|
||||
if (votesResult.data) {
|
||||
for (const v of votesResult.data) {
|
||||
userVotes.push(v.option_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const fixedOptions = [];
|
||||
if (options) {
|
||||
for (const opt of options) {
|
||||
fixedOptions.push({
|
||||
id: opt.id,
|
||||
text: opt.text,
|
||||
vote_count: parseInt(opt.vote_count, 10) || 0,
|
||||
sort_order: parseInt(opt.sort_order, 10) || 0
|
||||
});
|
||||
}
|
||||
for (const opt of options) {
|
||||
fixedOptions.push({
|
||||
id: opt.id,
|
||||
text: opt.text,
|
||||
vote_count: parseInt(opt.vote_count, 10) || 0,
|
||||
sort_order: parseInt(opt.sort_order, 10) || 0
|
||||
});
|
||||
}
|
||||
|
||||
return ok({
|
||||
@@ -328,27 +326,25 @@ export function castVote(input) {
|
||||
if (!userId) return fail(400, "user_id required");
|
||||
if (!optionIds || optionIds.length === 0) return fail(400, "option_ids required");
|
||||
|
||||
const polls = dbQuery("SELECT id, topic_id, question, max_choices, is_closed FROM forum_polls WHERE id = ?", [pollId]);
|
||||
if (!polls || polls.length === 0) return fail(404, "poll not found");
|
||||
const poll = dbFetchOne("forum_polls", { id: pollId });
|
||||
if (!poll) return fail(404, "poll not found");
|
||||
|
||||
const poll = polls[0];
|
||||
if (parseInt(poll.is_closed, 10) === 1) return fail(400, "poll is closed");
|
||||
|
||||
const maxChoices = parseInt(poll.max_choices, 10);
|
||||
if (optionIds.length > maxChoices) return fail(400, `too many choices (max ${maxChoices})`);
|
||||
|
||||
const existingVotes = dbQuery("SELECT option_id FROM forum_poll_votes WHERE poll_id = ? AND user_id = ?", [pollId, userId]);
|
||||
if (existingVotes?.length > 0) return fail(400, "already voted");
|
||||
const existingVotes = dbFetchAll("forum_poll_votes", { poll_id: pollId, user_id: userId });
|
||||
if (existingVotes.data && existingVotes.data.length > 0) return fail(400, "already voted");
|
||||
|
||||
for (const optId of optionIds) {
|
||||
const opts = dbQuery("SELECT id FROM forum_poll_options WHERE id = ? AND poll_id = ?", [optId, pollId]);
|
||||
if (!opts || opts.length === 0) return fail(400, `option not found: ${optId}`);
|
||||
const opt = dbFetchOne("forum_poll_options", { id: optId, poll_id: pollId });
|
||||
if (!opt) return fail(400, `option not found: ${optId}`);
|
||||
|
||||
const voteId = newId();
|
||||
const now = nowISO();
|
||||
dbExec("INSERT INTO forum_poll_votes (id, tenant_id, poll_id, option_id, user_id, created_at, updated_at) VALUES (?, 'default', ?, ?, ?, ?, ?)",
|
||||
[voteId, pollId, optId, userId, now, now]);
|
||||
dbExec("UPDATE forum_poll_options SET vote_count = vote_count + 1 WHERE id = ?", [optId]);
|
||||
dbInsert("forum_poll_votes", { id: voteId, tenant_id: "default", poll_id: pollId, option_id: optId, user_id: userId, created_at: now, updated_at: now });
|
||||
dbIncrement("forum_poll_options", { vote_count: 1 }, { id: optId });
|
||||
}
|
||||
|
||||
return ok({ poll_id: pollId, voted_options: optionIds });
|
||||
@@ -361,15 +357,15 @@ export function deletePoll(input) {
|
||||
|
||||
if (!userId) return fail(400, "user_id required");
|
||||
|
||||
const polls = dbQuery("SELECT id, topic_id FROM forum_polls WHERE id = ?", [pollId]);
|
||||
if (!polls || polls.length === 0) return fail(404, "poll not found");
|
||||
const poll = dbFetchOne("forum_polls", { id: pollId });
|
||||
if (!poll) return fail(404, "poll not found");
|
||||
|
||||
const topics = dbQuery("SELECT author_id FROM forum_topics WHERE id = ?", [polls[0].topic_id]);
|
||||
if (!topics || topics.length === 0 || topics[0].author_id !== userId) return fail(403, "only topic author can delete poll");
|
||||
const topic = dbFetchOne("forum_topics", { id: poll.topic_id });
|
||||
if (!topic || topic.author_id !== userId) return fail(403, "only topic author can delete poll");
|
||||
|
||||
dbExec("DELETE FROM forum_poll_votes WHERE poll_id = ?", [pollId]);
|
||||
dbExec("DELETE FROM forum_poll_options WHERE poll_id = ?", [pollId]);
|
||||
dbExec("DELETE FROM forum_polls WHERE id = ?", [pollId]);
|
||||
dbDelete("forum_poll_votes", { poll_id: pollId });
|
||||
dbDelete("forum_poll_options", { poll_id: pollId });
|
||||
dbDelete("forum_polls", { id: pollId });
|
||||
|
||||
return ok({ deleted: true });
|
||||
}
|
||||
|
||||
Vendored
+20
@@ -2,6 +2,17 @@ export interface DbExecResult {
|
||||
error?: string;
|
||||
rows_affected?: number;
|
||||
}
|
||||
export interface DbCrudResult {
|
||||
rows_affected?: number;
|
||||
}
|
||||
export interface DbFetchAllResult {
|
||||
data: Record<string, unknown>[];
|
||||
total: number;
|
||||
}
|
||||
export interface DbGroupByResult {
|
||||
data: Record<string, unknown>[];
|
||||
total: number;
|
||||
}
|
||||
export interface PluginError {
|
||||
__plugin_error: boolean;
|
||||
__status: number;
|
||||
@@ -10,6 +21,15 @@ export interface PluginError {
|
||||
export declare const SDK_VERSION: string;
|
||||
export declare function dbQuery(sql: string, params?: unknown[]): Record<string, unknown>[];
|
||||
export declare function dbExec(sql: string, params?: unknown[]): DbExecResult;
|
||||
export declare function dbInsert(table: string, data: Record<string, unknown> | string, options?: Record<string, unknown>): DbCrudResult;
|
||||
export declare function dbFetchOne(table: string, where: Record<string, unknown> | string | unknown[], options?: Record<string, unknown>): Record<string, unknown> | null;
|
||||
export declare function dbFetchAll(table: string, where?: Record<string, unknown> | string | unknown[] | null, options?: Record<string, unknown>): DbFetchAllResult;
|
||||
export declare function dbUpdate(table: string, data: Record<string, unknown> | string, where: Record<string, unknown> | string | unknown[], options?: Record<string, unknown>): DbCrudResult;
|
||||
export declare function dbDelete(table: string, where: Record<string, unknown> | string | unknown[], options?: Record<string, unknown>): DbCrudResult;
|
||||
export declare function dbCount(table: string, where?: Record<string, unknown> | string | unknown[] | null, options?: Record<string, unknown>): number;
|
||||
export declare function dbIncrement(table: string, columns: Record<string, number> | string, where?: Record<string, unknown> | string | unknown[] | null, options?: Record<string, unknown>): DbCrudResult;
|
||||
export declare function dbSum(table: string, column: string, where?: Record<string, unknown> | string | unknown[] | null, options?: Record<string, unknown>): number;
|
||||
export declare function dbGroupBy(table: string, options: Record<string, unknown> | string): DbGroupByResult;
|
||||
export declare function dbBegin(): {
|
||||
ok: boolean;
|
||||
};
|
||||
|
||||
@@ -17,6 +17,70 @@ export function dbExec(sql, params) {
|
||||
const result = RaisFastHost.dbExecute(sql, JSON.stringify(params ?? []));
|
||||
return JSON.parse(result);
|
||||
}
|
||||
export function dbInsert(table, data, options) {
|
||||
const dataStr = typeof data === "string" ? data : JSON.stringify(data);
|
||||
const optStr = options == null ? "{}" : (typeof options === "string" ? options : JSON.stringify(options));
|
||||
const result = JSON.parse(RaisFastHost.dbInsert(table, dataStr, optStr));
|
||||
if (result.error) throw new Error(result.error);
|
||||
return result;
|
||||
}
|
||||
export function dbFetchOne(table, where, options) {
|
||||
const whereStr = where == null ? "{}" : (typeof where === "string" ? where : JSON.stringify(where));
|
||||
const optStr = options == null ? "{}" : (typeof options === "string" ? options : JSON.stringify(options));
|
||||
const result = JSON.parse(RaisFastHost.dbFetchOne(table, whereStr, optStr));
|
||||
if (result.error) throw new Error(result.error);
|
||||
return result.data;
|
||||
}
|
||||
export function dbFetchAll(table, where, options) {
|
||||
const whereStr = where == null ? "{}" : (typeof where === "string" ? where : JSON.stringify(where));
|
||||
const optStr = options == null ? "{}" : (typeof options === "string" ? options : JSON.stringify(options));
|
||||
const result = JSON.parse(RaisFastHost.dbFetchAll(table, whereStr, optStr));
|
||||
if (result.error) throw new Error(result.error);
|
||||
return result;
|
||||
}
|
||||
export function dbUpdate(table, data, where, options) {
|
||||
const dataStr = typeof data === "string" ? data : JSON.stringify(data);
|
||||
const whereStr = where == null ? "{}" : (typeof where === "string" ? where : JSON.stringify(where));
|
||||
const optStr = options == null ? "{}" : (typeof options === "string" ? options : JSON.stringify(options));
|
||||
const result = JSON.parse(RaisFastHost.dbUpdate(table, dataStr, whereStr, optStr));
|
||||
if (result.error) throw new Error(result.error);
|
||||
return result;
|
||||
}
|
||||
export function dbDelete(table, where, options) {
|
||||
const whereStr = where == null ? "{}" : (typeof where === "string" ? where : JSON.stringify(where));
|
||||
const optStr = options == null ? "{}" : (typeof options === "string" ? options : JSON.stringify(options));
|
||||
const result = JSON.parse(RaisFastHost.dbDelete(table, whereStr, optStr));
|
||||
if (result.error) throw new Error(result.error);
|
||||
return result;
|
||||
}
|
||||
export function dbCount(table, where, options) {
|
||||
const whereStr = where == null ? "{}" : (typeof where === "string" ? where : JSON.stringify(where));
|
||||
const optStr = options == null ? "{}" : (typeof options === "string" ? options : JSON.stringify(options));
|
||||
const result = JSON.parse(RaisFastHost.dbCount(table, whereStr, optStr));
|
||||
if (result.error) throw new Error(result.error);
|
||||
return result.count;
|
||||
}
|
||||
export function dbIncrement(table, columns, where, options) {
|
||||
const colStr = typeof columns === "string" ? columns : JSON.stringify(columns);
|
||||
const whereStr = where == null ? "{}" : (typeof where === "string" ? where : JSON.stringify(where));
|
||||
const optStr = options == null ? "{}" : (typeof options === "string" ? options : JSON.stringify(options));
|
||||
const result = JSON.parse(RaisFastHost.dbIncrement(table, colStr, whereStr, optStr));
|
||||
if (result.error) throw new Error(result.error);
|
||||
return result;
|
||||
}
|
||||
export function dbSum(table, column, where, options) {
|
||||
const whereStr = where == null ? "{}" : (typeof where === "string" ? where : JSON.stringify(where));
|
||||
const optStr = options == null ? "{}" : (typeof options === "string" ? options : JSON.stringify(options));
|
||||
const result = JSON.parse(RaisFastHost.dbSum(table, column, whereStr, optStr));
|
||||
if (result.error) throw new Error(result.error);
|
||||
return result.sum;
|
||||
}
|
||||
export function dbGroupBy(table, options) {
|
||||
const optStr = options == null ? "{}" : (typeof options === "string" ? options : JSON.stringify(options));
|
||||
const result = JSON.parse(RaisFastHost.dbGroupBy(table, optStr));
|
||||
if (result.error) throw new Error(result.error);
|
||||
return result;
|
||||
}
|
||||
export function dbBegin() {
|
||||
const result = JSON.parse(RaisFastHost.dbBegin());
|
||||
if (!result.ok)
|
||||
|
||||
@@ -19,6 +19,79 @@ function M.dbExec(sql, params)
|
||||
return RaisFastHost.jsonDecode(result)
|
||||
end
|
||||
|
||||
function M.dbInsert(table, data, options)
|
||||
local dataStr = type(data) == "string" and data or RaisFastHost.jsonEncode(data)
|
||||
local optStr = (options == nil) and "{}" or (type(options) == "string" and options or RaisFastHost.jsonEncode(options))
|
||||
local result = RaisFastHost.jsonDecode(RaisFastHost.dbInsert(table, dataStr, optStr))
|
||||
if result.error then error(result.error) end
|
||||
return result
|
||||
end
|
||||
|
||||
function M.dbFetchOne(table, where, options)
|
||||
local whereStr = (where == nil) and "{}" or (type(where) == "string" and where or RaisFastHost.jsonEncode(where))
|
||||
local optStr = (options == nil) and "{}" or (type(options) == "string" and options or RaisFastHost.jsonEncode(options))
|
||||
local result = RaisFastHost.jsonDecode(RaisFastHost.dbFetchOne(table, whereStr, optStr))
|
||||
if result.error then error(result.error) end
|
||||
return result.data
|
||||
end
|
||||
|
||||
function M.dbFetchAll(table, where, options)
|
||||
local whereStr = (where == nil) and "{}" or (type(where) == "string" and where or RaisFastHost.jsonEncode(where))
|
||||
local optStr = (options == nil) and "{}" or (type(options) == "string" and options or RaisFastHost.jsonEncode(options))
|
||||
local result = RaisFastHost.jsonDecode(RaisFastHost.dbFetchAll(table, whereStr, optStr))
|
||||
if result.error then error(result.error) end
|
||||
return result
|
||||
end
|
||||
|
||||
function M.dbUpdate(table, data, where, options)
|
||||
local dataStr = type(data) == "string" and data or RaisFastHost.jsonEncode(data)
|
||||
local whereStr = (where == nil) and "{}" or (type(where) == "string" and where or RaisFastHost.jsonEncode(where))
|
||||
local optStr = (options == nil) and "{}" or (type(options) == "string" and options or RaisFastHost.jsonEncode(options))
|
||||
local result = RaisFastHost.jsonDecode(RaisFastHost.dbUpdate(table, dataStr, whereStr, optStr))
|
||||
if result.error then error(result.error) end
|
||||
return result
|
||||
end
|
||||
|
||||
function M.dbDelete(table, where, options)
|
||||
local whereStr = (where == nil) and "{}" or (type(where) == "string" and where or RaisFastHost.jsonEncode(where))
|
||||
local optStr = (options == nil) and "{}" or (type(options) == "string" and options or RaisFastHost.jsonEncode(options))
|
||||
local result = RaisFastHost.jsonDecode(RaisFastHost.dbDelete(table, whereStr, optStr))
|
||||
if result.error then error(result.error) end
|
||||
return result
|
||||
end
|
||||
|
||||
function M.dbCount(table, where, options)
|
||||
local whereStr = (where == nil) and "{}" or (type(where) == "string" and where or RaisFastHost.jsonEncode(where))
|
||||
local optStr = (options == nil) and "{}" or (type(options) == "string" and options or RaisFastHost.jsonEncode(options))
|
||||
local result = RaisFastHost.jsonDecode(RaisFastHost.dbCount(table, whereStr, optStr))
|
||||
if result.error then error(result.error) end
|
||||
return result.count
|
||||
end
|
||||
|
||||
function M.dbIncrement(table, columns, where, options)
|
||||
local colStr = type(columns) == "string" and columns or RaisFastHost.jsonEncode(columns)
|
||||
local whereStr = (where == nil) and "{}" or (type(where) == "string" and where or RaisFastHost.jsonEncode(where))
|
||||
local optStr = (options == nil) and "{}" or (type(options) == "string" and options or RaisFastHost.jsonEncode(options))
|
||||
local result = RaisFastHost.jsonDecode(RaisFastHost.dbIncrement(table, colStr, whereStr, optStr))
|
||||
if result.error then error(result.error) end
|
||||
return result
|
||||
end
|
||||
|
||||
function M.dbSum(table, column, where, options)
|
||||
local whereStr = (where == nil) and "{}" or (type(where) == "string" and where or RaisFastHost.jsonEncode(where))
|
||||
local optStr = (options == nil) and "{}" or (type(options) == "string" and options or RaisFastHost.jsonEncode(options))
|
||||
local result = RaisFastHost.jsonDecode(RaisFastHost.dbSum(table, column, whereStr, optStr))
|
||||
if result.error then error(result.error) end
|
||||
return result.sum
|
||||
end
|
||||
|
||||
function M.dbGroupBy(table, options)
|
||||
local optStr = (options == nil) and "{}" or (type(options) == "string" and options or RaisFastHost.jsonEncode(options))
|
||||
local result = RaisFastHost.jsonDecode(RaisFastHost.dbGroupBy(table, optStr))
|
||||
if result.error then error(result.error) end
|
||||
return result
|
||||
end
|
||||
|
||||
function M.dbBegin()
|
||||
local result = RaisFastHost.jsonDecode(RaisFastHost.dbBegin())
|
||||
if not result.ok then error("dbBegin failed") end
|
||||
|
||||
+10
-12
@@ -53,18 +53,16 @@ pub async fn find_by_user_and_product(
|
||||
)
|
||||
.map_err(Into::into)
|
||||
} else {
|
||||
let sql = if tenant_id.is_some() {
|
||||
"SELECT * FROM cart_items WHERE user_id = ? AND product_id = ? AND variant_id IS NULL AND tenant_id = ?"
|
||||
} else {
|
||||
"SELECT * FROM cart_items WHERE user_id = ? AND product_id = ? AND variant_id IS NULL"
|
||||
};
|
||||
let mut q = sqlx::query_as::<_, CartItem>(sql)
|
||||
.bind(user_id)
|
||||
.bind(product_id);
|
||||
if let Some(tid) = tenant_id {
|
||||
q = q.bind(tid);
|
||||
}
|
||||
Ok(q.fetch_optional(pool).await?)
|
||||
raisfast_derive::crud_find!(
|
||||
pool,
|
||||
"cart_items",
|
||||
CartItem,
|
||||
"user_id" => user_id,
|
||||
and: ["product_id" => product_id],
|
||||
and_null: ["variant_id"],
|
||||
tenant: tenant_id
|
||||
)
|
||||
.map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -98,6 +98,85 @@ pub fn register_host_functions(
|
||||
let db_rollback_fn = Function::new(ctx.clone(), move || -> String { hc.db_rollback() })?;
|
||||
host.set("dbRollback", db_rollback_fn)?;
|
||||
|
||||
let hc = host_ctx.clone();
|
||||
let db_insert_fn = Function::new(
|
||||
ctx.clone(),
|
||||
move |table: String, data: String, options: String| -> String {
|
||||
hc.db_insert(&table, &data, &options)
|
||||
},
|
||||
)?;
|
||||
host.set("dbInsert", db_insert_fn)?;
|
||||
|
||||
let hc = host_ctx.clone();
|
||||
let db_fetch_one_fn = Function::new(
|
||||
ctx.clone(),
|
||||
move |table: String, r#where: String, options: String| -> String {
|
||||
hc.db_fetch_one(&table, &r#where, &options)
|
||||
},
|
||||
)?;
|
||||
host.set("dbFetchOne", db_fetch_one_fn)?;
|
||||
|
||||
let hc = host_ctx.clone();
|
||||
let db_fetch_all_fn = Function::new(
|
||||
ctx.clone(),
|
||||
move |table: String, r#where: String, options: String| -> String {
|
||||
hc.db_fetch_all(&table, &r#where, &options)
|
||||
},
|
||||
)?;
|
||||
host.set("dbFetchAll", db_fetch_all_fn)?;
|
||||
|
||||
let hc = host_ctx.clone();
|
||||
let db_update_fn = Function::new(
|
||||
ctx.clone(),
|
||||
move |table: String, data: String, r#where: String, options: String| -> String {
|
||||
hc.db_update(&table, &data, &r#where, &options)
|
||||
},
|
||||
)?;
|
||||
host.set("dbUpdate", db_update_fn)?;
|
||||
|
||||
let hc = host_ctx.clone();
|
||||
let db_delete_fn = Function::new(
|
||||
ctx.clone(),
|
||||
move |table: String, r#where: String, options: String| -> String {
|
||||
hc.db_delete(&table, &r#where, &options)
|
||||
},
|
||||
)?;
|
||||
host.set("dbDelete", db_delete_fn)?;
|
||||
|
||||
let hc = host_ctx.clone();
|
||||
let db_count_fn = Function::new(
|
||||
ctx.clone(),
|
||||
move |table: String, r#where: String, options: String| -> String {
|
||||
hc.db_count(&table, &r#where, &options)
|
||||
},
|
||||
)?;
|
||||
host.set("dbCount", db_count_fn)?;
|
||||
|
||||
let hc = host_ctx.clone();
|
||||
let db_increment_fn = Function::new(
|
||||
ctx.clone(),
|
||||
move |table: String, columns: String, r#where: String, options: String| -> String {
|
||||
hc.db_increment(&table, &columns, &r#where, &options)
|
||||
},
|
||||
)?;
|
||||
host.set("dbIncrement", db_increment_fn)?;
|
||||
|
||||
let hc = host_ctx.clone();
|
||||
let db_sum_fn = Function::new(
|
||||
ctx.clone(),
|
||||
move |table: String, column: String, r#where: String, options: String| -> String {
|
||||
hc.db_sum(&table, &column, &r#where, &options)
|
||||
},
|
||||
)?;
|
||||
host.set("dbSum", db_sum_fn)?;
|
||||
|
||||
let hc = host_ctx.clone();
|
||||
let db_group_by_fn = Function::new(
|
||||
ctx.clone(),
|
||||
move |table: String, options: String| -> String { hc.db_group_by(&table, &options) },
|
||||
)?;
|
||||
host.set("dbGroupBy", db_group_by_fn)?;
|
||||
|
||||
let hc = host_ctx.clone();
|
||||
let vfs_read_fn = Function::new(ctx.clone(), move |path: String| -> Option<String> {
|
||||
hc.vfs_read(&path).ok()
|
||||
|
||||
@@ -113,6 +113,94 @@ pub fn register_host_functions(
|
||||
})?;
|
||||
host.set("dbRollback", db_rollback_fn)?;
|
||||
|
||||
let hc = host_ctx.clone();
|
||||
let db_insert_fn = lua.create_function(
|
||||
move |lua, (table, data, options): (String, String, String)| {
|
||||
Ok(mlua::Value::String(
|
||||
lua.create_string(hc.db_insert(&table, &data, &options))?,
|
||||
))
|
||||
},
|
||||
)?;
|
||||
host.set("dbInsert", db_insert_fn)?;
|
||||
|
||||
let hc = host_ctx.clone();
|
||||
let db_fetch_one_fn = lua.create_function(
|
||||
move |lua, (table, r#where, options): (String, String, String)| {
|
||||
Ok(mlua::Value::String(lua.create_string(
|
||||
hc.db_fetch_one(&table, &r#where, &options),
|
||||
)?))
|
||||
},
|
||||
)?;
|
||||
host.set("dbFetchOne", db_fetch_one_fn)?;
|
||||
|
||||
let hc = host_ctx.clone();
|
||||
let db_fetch_all_fn = lua.create_function(
|
||||
move |lua, (table, r#where, options): (String, String, String)| {
|
||||
Ok(mlua::Value::String(lua.create_string(
|
||||
hc.db_fetch_all(&table, &r#where, &options),
|
||||
)?))
|
||||
},
|
||||
)?;
|
||||
host.set("dbFetchAll", db_fetch_all_fn)?;
|
||||
|
||||
let hc = host_ctx.clone();
|
||||
let db_update_fn = lua.create_function(
|
||||
move |lua, (table, data, r#where, options): (String, String, String, String)| {
|
||||
Ok(mlua::Value::String(lua.create_string(
|
||||
hc.db_update(&table, &data, &r#where, &options),
|
||||
)?))
|
||||
},
|
||||
)?;
|
||||
host.set("dbUpdate", db_update_fn)?;
|
||||
|
||||
let hc = host_ctx.clone();
|
||||
let db_delete_fn = lua.create_function(
|
||||
move |lua, (table, r#where, options): (String, String, String)| {
|
||||
Ok(mlua::Value::String(
|
||||
lua.create_string(hc.db_delete(&table, &r#where, &options))?,
|
||||
))
|
||||
},
|
||||
)?;
|
||||
host.set("dbDelete", db_delete_fn)?;
|
||||
|
||||
let hc = host_ctx.clone();
|
||||
let db_count_fn = lua.create_function(
|
||||
move |lua, (table, r#where, options): (String, String, String)| {
|
||||
Ok(mlua::Value::String(
|
||||
lua.create_string(hc.db_count(&table, &r#where, &options))?,
|
||||
))
|
||||
},
|
||||
)?;
|
||||
host.set("dbCount", db_count_fn)?;
|
||||
|
||||
let hc = host_ctx.clone();
|
||||
let db_increment_fn = lua.create_function(
|
||||
move |lua, (table, columns, r#where, options): (String, String, String, String)| {
|
||||
Ok(mlua::Value::String(lua.create_string(
|
||||
hc.db_increment(&table, &columns, &r#where, &options),
|
||||
)?))
|
||||
},
|
||||
)?;
|
||||
host.set("dbIncrement", db_increment_fn)?;
|
||||
|
||||
let hc = host_ctx.clone();
|
||||
let db_sum_fn = lua.create_function(
|
||||
move |lua, (table, column, r#where, options): (String, String, String, String)| {
|
||||
Ok(mlua::Value::String(lua.create_string(
|
||||
hc.db_sum(&table, &column, &r#where, &options),
|
||||
)?))
|
||||
},
|
||||
)?;
|
||||
host.set("dbSum", db_sum_fn)?;
|
||||
|
||||
let hc = host_ctx.clone();
|
||||
let db_group_by_fn = lua.create_function(move |lua, (table, options): (String, String)| {
|
||||
Ok(mlua::Value::String(
|
||||
lua.create_string(hc.db_group_by(&table, &options))?,
|
||||
))
|
||||
})?;
|
||||
host.set("dbGroupBy", db_group_by_fn)?;
|
||||
|
||||
let hc = host_ctx.clone();
|
||||
let vfs_read_fn = lua.create_function(move |lua, path: String| match hc.vfs_read(&path) {
|
||||
Ok(content) => Ok(mlua::Value::String(lua.create_string(&content)?)),
|
||||
|
||||
@@ -92,6 +92,75 @@ pub fn register_host_functions(
|
||||
let hc = host_ctx.clone();
|
||||
engine.register_fn("dbRollback", move || -> String { hc.db_rollback() });
|
||||
|
||||
let hc = host_ctx.clone();
|
||||
engine.register_fn(
|
||||
"dbInsert",
|
||||
move |table: &str, data: &str, options: &str| -> String {
|
||||
hc.db_insert(table, data, options)
|
||||
},
|
||||
);
|
||||
|
||||
let hc = host_ctx.clone();
|
||||
engine.register_fn(
|
||||
"dbFetchOne",
|
||||
move |table: &str, r#where: &str, options: &str| -> String {
|
||||
hc.db_fetch_one(table, r#where, options)
|
||||
},
|
||||
);
|
||||
|
||||
let hc = host_ctx.clone();
|
||||
engine.register_fn(
|
||||
"dbFetchAll",
|
||||
move |table: &str, r#where: &str, options: &str| -> String {
|
||||
hc.db_fetch_all(table, r#where, options)
|
||||
},
|
||||
);
|
||||
|
||||
let hc = host_ctx.clone();
|
||||
engine.register_fn(
|
||||
"dbUpdate",
|
||||
move |table: &str, data: &str, r#where: &str, options: &str| -> String {
|
||||
hc.db_update(table, data, r#where, options)
|
||||
},
|
||||
);
|
||||
|
||||
let hc = host_ctx.clone();
|
||||
engine.register_fn(
|
||||
"dbDelete",
|
||||
move |table: &str, r#where: &str, options: &str| -> String {
|
||||
hc.db_delete(table, r#where, options)
|
||||
},
|
||||
);
|
||||
|
||||
let hc = host_ctx.clone();
|
||||
engine.register_fn(
|
||||
"dbCount",
|
||||
move |table: &str, r#where: &str, options: &str| -> String {
|
||||
hc.db_count(table, r#where, options)
|
||||
},
|
||||
);
|
||||
|
||||
let hc = host_ctx.clone();
|
||||
engine.register_fn(
|
||||
"dbIncrement",
|
||||
move |table: &str, columns: &str, r#where: &str, options: &str| -> String {
|
||||
hc.db_increment(table, columns, r#where, options)
|
||||
},
|
||||
);
|
||||
|
||||
let hc = host_ctx.clone();
|
||||
engine.register_fn(
|
||||
"dbSum",
|
||||
move |table: &str, column: &str, r#where: &str, options: &str| -> String {
|
||||
hc.db_sum(table, column, r#where, options)
|
||||
},
|
||||
);
|
||||
|
||||
let hc = host_ctx.clone();
|
||||
engine.register_fn("dbGroupBy", move |table: &str, options: &str| -> String {
|
||||
hc.db_group_by(table, options)
|
||||
});
|
||||
|
||||
let hc = host_ctx.clone();
|
||||
engine.register_fn("vfsRead", move |path: &str| -> rhai::Dynamic {
|
||||
match hc.vfs_read(path) {
|
||||
@@ -294,6 +363,12 @@ mod tests {
|
||||
let _: String = engine.eval(r#"dbCommit()"#).unwrap();
|
||||
let _: String = engine.eval(r#"dbRollback()"#).unwrap();
|
||||
let _: String = engine.eval(r#"dbPh(1)"#).unwrap();
|
||||
let _: String = engine.eval(r#"dbInsert("t", "{}", "{}")"#).unwrap();
|
||||
let _: String = engine.eval(r#"dbFetchOne("t", "{}", "{}")"#).unwrap();
|
||||
let _: String = engine.eval(r#"dbFetchAll("t", "{}", "{}")"#).unwrap();
|
||||
let _: String = engine.eval(r#"dbUpdate("t", "{}", "{}", "{}")"#).unwrap();
|
||||
let _: String = engine.eval(r#"dbDelete("t", "{}", "{}")"#).unwrap();
|
||||
let _: String = engine.eval(r#"dbCount("t", "{}", "{}")"#).unwrap();
|
||||
let _ = engine.eval::<rhai::Dynamic>(r#"vfsRead("f")"#).unwrap();
|
||||
let _: bool = engine.eval(r#"vfsWrite("f", "c")"#).unwrap();
|
||||
let _: bool = engine.eval(r#"vfsDelete("f")"#).unwrap();
|
||||
|
||||
Reference in New Issue
Block a user