5.7 KiB
5.7 KiB
| name | description | version |
|---|---|---|
| sage-module-scaffolding | Use when building a new Sage module from scratch. | 1.0.0 |
Sage Module Scaffolding
Complete recipe for creating a new Sage module. Derived from building storage_mgr and image_mgr.
File Structure
module_name/ ← repo root
├── setup.json ← module metadata
├── module_name/ ← Python package (matches packages[] in setup.json)
│ └── __init__.py ← core async functions called by DSPY
├── models/ ← DB table definitions (one JSON per table)
│ └── table_name.json
├── json/ ← CRUD browser configs (one JSON per table alias)
│ └── table_name_list.json
├── wwwroot/
│ └── api/ ← custom DSPY endpoints
│ └── custom_action.dspy
├── scripts/
│ └── load_path.py ← RBAC permission registration
└── README.md
Step 1: setup.json
{
"name": "module_name",
"version": "0.1.0",
"description": "简短中文描述",
"packages": ["module_name"],
"install_requires": ["apppublic", "sqlor", "ahserver", "appbase"],
"python_requires": ">=3.10"
}
Pitfall: packages must match Python directory name. install_requires always includes apppublic, sqlor, ahserver, appbase. Add rbac if needed.
Step 2: models/*.json — Data Models
One JSON per DB table. Three sections: summary, fields, codes.
{
"summary": [{
"name": "table_name",
"title": "中文表名",
"primary": ["id"],
"catelog": "entity"
}],
"fields": [
{"name": "id", "title": "id", "type": "str", "length": 32, "nullable": "no"},
{"name": "resellerid", "title": "商户机构id", "type": "str", "length": 32, "nullable": "no"},
{"name": "name", "title": "名称", "type": "str", "length": 128, "nullable": "no"},
{"name": "status", "title": "状态", "type": "char", "length": 16, "default": "active"},
{"name": "created_at", "title": "创建时间", "type": "timestamp", "nullable": "no"},
{"name": "updated_at", "title": "更新时间", "type": "timestamp", "nullable": "no"}
],
"codes": [
{"field": "status", "table": "appcodes_kv", "valuefield": "k", "textfield": "v", "cond": "parentid='my_enum'"},
{"field": "fk_col", "table": "foreign_table", "valuefield": "id", "textfield": "name"}
]
}
Rules:
- EVERY table MUST have:
id(str 32),resellerid(str 32),created_at,updated_at catelog: "entity"= standard CRUD tablecodes: enums →appcodes_kvwithcond; FKs → target table name- Types: str, char, int, bigint, text, timestamp
Step 3: json/*_list.json — Browser Configs
{
"tblname": "table_name",
"alias": "table_name_list",
"title": "页面标题",
"params": {
"browserfields": {
"name": {"title": "名称", "width": 150},
"status": {"title": "状态", "width": 80}
},
"editexclouded": ["id", "resellerid", "created_at", "updated_at"],
"toolbar": {"tools": []},
"binds": [],
"new_data_url": "{{entire_url('/module_name/api/table_name_create.dspy')}}",
"update_data_url": "{{entire_url('/module_name/api/table_name_update.dspy')}}",
"delete_data_url": "{{entire_url('/module_name/api/table_name_delete.dspy')}}",
"logined_userorgid": "resellerid"
}
}
Rules:
- URL pattern:
/module_name/api/{tblname}_{action}.dspy - CRUD DSPY files are framework-handled — no need to create files unless custom logic
logined_userorgid: "resellerid"= auto-filter by org- Pitfall:
tblnamemismatch between model JSON and browser JSON = silent failure
Step 4: wwwroot/api/*.dspy — Custom Endpoints
result = await function_name(request, params_kw)
return result if isinstance(result, dict) else {'widgettype': 'Error', 'options': {'title': '失败', 'message': str(result)}}
DSPY safety rules:
- NO f-strings — use concatenation or
.format() - NO bare
import— usefromimports - Built-in globals available:
json,uuid,DBPools,os,request uuid()no-args for IDs, NOTuuid.uuid4()- Always guard with
isinstance(result, dict)
Step 5: scripts/load_path.py
Copy from any existing module, change only mod_name. Auto-discovers CRUD aliases from json/ and APIs from wwwroot/api/. Registers paths for roles: any, logined, reseller.operator.
Step 6: module_name/init.py — Core Logic
import datetime, json, asyncio
from appPublic.uniqueID import getID
from sqlor.dbpools import DBPools
MODULE_NAME = 'pccs' # Sage app DB namespace, NOT module name
async def some_function(request, params_kw):
env = request._run_ns
dbname = env.get_module_dbname(MODULE_NAME)
async with DBPools().sqlorContext(dbname) as sor:
recs = await sor.R('table_name', {'status': 'active'})
return {'status': 'ok', 'data': '...'}
Key conventions:
MODULE_NAME = 'pccs'= Sage app DB name, NOT the module's own namerequest._run_nsgives Sage runtime environmentsqlor: C/R/U/D for CRUD;sqlExe(sql, params)for complex queries —${var}$placeholdersgetID()from appPublic.uniqueID for new record IDs- Pitfall: forgetting
awaiton sqlor methods returns coroutines instead of results
Deployment
python scripts/load_path.py— RBAC registration- Commit + push to git
- Server:
git pull, restart Sage (orpip install -e .) - Verify:
/module_name/table_name_list/index.ui
Reference Examples
references/storage-mgr-example.md— Shared storage management module (3 tables, 3 custom APIs)references/image-mgr-example.md— Container image management module (3 tables, 6 custom APIs, 8 preset domestic mirrors)