add database crud operation function to plugin system

This commit is contained in:
zhongqin
2026-05-19 02:20:48 +08:00
parent 493be8f7de
commit 15c6094de7
22 changed files with 2201 additions and 1002 deletions
@@ -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
View File
@@ -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) });
}
+20
View File
@@ -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"]
}
-200
View File
@@ -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
View File
@@ -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;
+13 -28
View File
@@ -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
View File
@@ -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
View File
@@ -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 });
}
+20
View File
@@ -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;
};
+64
View File
@@ -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)
+73
View File
@@ -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
View File
@@ -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
+79
View File
@@ -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()
+88
View File
@@ -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)?)),
+75
View File
@@ -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();