310 lines
13 KiB
Python
310 lines
13 KiB
Python
# -*- coding: utf-8 -*-
|
||
"""M1b API 层:模板平台公共部分 / 子对象扩展 / 关联表。
|
||
|
||
约定
|
||
----
|
||
* 每个 handler 第一行取 tenant_id(来自登录上下文 params['tenant_id'] 或 session),
|
||
缺失直接 400 fail-closed,绝不做全租户扫描;
|
||
* ``is_platform_admin`` 只信 RBAC 判定结果(由应用层注入 params),不接受前端自述;
|
||
* 返回统一 {ok, ...} / 异常转 {ok: False, code, message}。
|
||
|
||
路由(挂在模块 api 前缀下)
|
||
GET /pbl/templates 列表(本租户 + 平台公共)
|
||
POST /pbl/templates 创建(scope=platform 需平台管理员)
|
||
GET /pbl/templates/{id} 详情
|
||
PUT /pbl/templates/{id} 更新
|
||
DEL /pbl/templates/{id} 软删(内置禁删)
|
||
POST /pbl/templates/{id}/publish 发布
|
||
POST /pbl/templates/{id}/deprecate 停用
|
||
POST /pbl/templates/{id}/fork 派生为租户私有
|
||
POST /pbl/templates/{id}/instantiate 实例化为蓝图
|
||
GET /pbl/ext-defs 扩展字段定义列表
|
||
POST /pbl/ext-defs 创建定义
|
||
PUT /pbl/ext-defs/{id} 更新定义
|
||
DEL /pbl/ext-defs/{id} 删除定义
|
||
GET /pbl/blueprints/{bp}/ext 蓝图扩展值(按子对象聚合)
|
||
PUT /pbl/blueprints/{bp}/ext 批量写扩展值
|
||
DEL /pbl/blueprints/{bp}/ext 删扩展值
|
||
GET /pbl/blueprints/{bp}/refs 关联边列表
|
||
POST /pbl/blueprints/{bp}/refs 新增/批量新增关联边
|
||
DEL /pbl/refs/{id} 删除关联边
|
||
POST /pbl/blueprints/{bp}/refs/resolve 引用有效性判定
|
||
POST /pbl/blueprints/{bp}/refs/sync 按 content 同步关联边
|
||
GET /pbl/refs/impact 反查影响面(供 world 删除前置校验)
|
||
"""
|
||
|
||
from .m1b_common import PblM1bError, require_tenant
|
||
from . import m1b_template as T
|
||
from . import m1b_subobject as S
|
||
from . import m1b_ref as R
|
||
|
||
__all__ = ["M1B_ROUTES", "dispatch", "register_m1b_routes"]
|
||
|
||
|
||
def _ctx(params):
|
||
params = dict(params or {})
|
||
tenant_id = params.get("tenant_id") or params.get("tenantId")
|
||
actor = params.get("actor") or params.get("user_id") or params.get("operator")
|
||
is_admin = params.get("is_platform_admin")
|
||
if isinstance(is_admin, str):
|
||
is_admin = is_admin.strip().lower() in ("1", "true", "yes")
|
||
return tenant_id, actor, bool(is_admin), params
|
||
|
||
|
||
def _ok(**kw):
|
||
out = {"ok": True}
|
||
out.update(kw)
|
||
return out
|
||
|
||
|
||
# --------------------------------------------------------------------------
|
||
# handlers
|
||
# --------------------------------------------------------------------------
|
||
def api_list_templates(db, params):
|
||
tenant_id, _actor, _adm, p = _ctx(params)
|
||
return T.list_templates(db, tenant_id,
|
||
include_platform=p.get("include_platform", True) not in (False, "false", 0),
|
||
status=p.get("status"), category=p.get("category"),
|
||
keyword=p.get("keyword"),
|
||
limit=int(p.get("limit") or 100), offset=int(p.get("offset") or 0))
|
||
|
||
|
||
def api_get_template(db, params):
|
||
tenant_id, _a, _adm, p = _ctx(params)
|
||
return _ok(template=T.get_template(db, tenant_id, p.get("template_id") or p.get("id")))
|
||
|
||
|
||
def api_create_template(db, params):
|
||
tenant_id, actor, adm, p = _ctx(params)
|
||
payload = dict(p.get("payload") or p.get("data") or p)
|
||
payload.pop("tenant_id", None)
|
||
return T.create_template(db, tenant_id, payload, actor=actor, is_platform_admin=adm)
|
||
|
||
|
||
def api_update_template(db, params):
|
||
tenant_id, actor, adm, p = _ctx(params)
|
||
payload = dict(p.get("payload") or p.get("data") or p)
|
||
for k in ("tenant_id", "template_id", "id", "actor", "is_platform_admin"):
|
||
payload.pop(k, None)
|
||
return T.update_template(db, tenant_id, p.get("template_id") or p.get("id"), payload,
|
||
actor=actor, is_platform_admin=adm)
|
||
|
||
|
||
def api_delete_template(db, params):
|
||
tenant_id, actor, adm, p = _ctx(params)
|
||
return T.delete_template(db, tenant_id, p.get("template_id") or p.get("id"),
|
||
actor=actor, is_platform_admin=adm)
|
||
|
||
|
||
def api_publish_template(db, params):
|
||
tenant_id, actor, adm, p = _ctx(params)
|
||
return T.publish_template(db, tenant_id, p.get("template_id") or p.get("id"),
|
||
actor=actor, is_platform_admin=adm)
|
||
|
||
|
||
def api_deprecate_template(db, params):
|
||
tenant_id, actor, adm, p = _ctx(params)
|
||
return T.deprecate_template(db, tenant_id, p.get("template_id") or p.get("id"),
|
||
actor=actor, is_platform_admin=adm)
|
||
|
||
|
||
def api_fork_template(db, params):
|
||
tenant_id, actor, _adm, p = _ctx(params)
|
||
return T.fork_template(db, tenant_id, p.get("template_id") or p.get("id"), actor=actor,
|
||
new_code=p.get("new_code"), new_name=p.get("new_name"))
|
||
|
||
|
||
def api_instantiate_template(db, params):
|
||
tenant_id, actor, _adm, p = _ctx(params)
|
||
create_blueprint = p.get("create_blueprint")
|
||
if not callable(create_blueprint):
|
||
try:
|
||
from .blueprint_crud import create_blueprint as _cb # type: ignore
|
||
create_blueprint = _cb
|
||
except Exception:
|
||
create_blueprint = None
|
||
return T.instantiate_template(db, tenant_id, p.get("template_id") or p.get("id"),
|
||
blueprint_payload=p.get("payload") or p.get("blueprint") or {},
|
||
actor=actor, create_blueprint=create_blueprint)
|
||
|
||
|
||
def api_list_ext_defs(db, params):
|
||
tenant_id, _a, _adm, p = _ctx(params)
|
||
return S.list_ext_field_defs(db, tenant_id, kind=p.get("kind") or p.get("subobject_kind"),
|
||
include_platform=p.get("include_platform", True) not in (False, "false", 0))
|
||
|
||
|
||
def api_create_ext_def(db, params):
|
||
tenant_id, actor, adm, p = _ctx(params)
|
||
payload = dict(p.get("payload") or p.get("data") or p)
|
||
payload.pop("tenant_id", None)
|
||
return S.create_ext_field_def(db, tenant_id, payload, actor=actor, is_platform_admin=adm)
|
||
|
||
|
||
def api_update_ext_def(db, params):
|
||
tenant_id, actor, adm, p = _ctx(params)
|
||
payload = dict(p.get("payload") or p.get("data") or p)
|
||
for k in ("tenant_id", "def_id", "id", "actor", "is_platform_admin"):
|
||
payload.pop(k, None)
|
||
return S.update_ext_field_def(db, tenant_id, p.get("def_id") or p.get("id"), payload,
|
||
actor=actor, is_platform_admin=adm)
|
||
|
||
|
||
def api_delete_ext_def(db, params):
|
||
tenant_id, actor, adm, p = _ctx(params)
|
||
return S.delete_ext_field_def(db, tenant_id, p.get("def_id") or p.get("id"),
|
||
actor=actor, is_platform_admin=adm)
|
||
|
||
|
||
def api_get_blueprint_ext(db, params):
|
||
tenant_id, _a, _adm, p = _ctx(params)
|
||
bp = p.get("blueprint_id") or p.get("bp_id")
|
||
if p.get("flat"):
|
||
return S.list_ext(db, tenant_id, blueprint_id=bp, kind=p.get("kind"),
|
||
subobject_id=p.get("subobject_id"), version_id=p.get("version_id"))
|
||
return S.subobject_tree_ext(db, tenant_id, bp, version_id=p.get("version_id"))
|
||
|
||
|
||
def api_set_blueprint_ext(db, params):
|
||
tenant_id, actor, _adm, p = _ctx(params)
|
||
bp = p.get("blueprint_id") or p.get("bp_id")
|
||
kind = p.get("kind") or p.get("subobject_kind")
|
||
sid = p.get("subobject_id")
|
||
values = p.get("values")
|
||
if isinstance(values, dict) and values:
|
||
return S.bulk_set_ext(db, tenant_id, bp, kind, sid, values, actor=actor,
|
||
source=p.get("source") or "manual", version_id=p.get("version_id"),
|
||
source_template_id=p.get("source_template_id"))
|
||
return S.set_ext(db, tenant_id, bp, kind, sid, p.get("ext_key"), p.get("value"),
|
||
actor=actor, source=p.get("source") or "manual",
|
||
version_id=p.get("version_id"),
|
||
source_template_id=p.get("source_template_id"))
|
||
|
||
|
||
def api_delete_blueprint_ext(db, params):
|
||
tenant_id, actor, _adm, p = _ctx(params)
|
||
return S.delete_ext(db, tenant_id, p.get("blueprint_id") or p.get("bp_id"),
|
||
p.get("kind") or p.get("subobject_kind"), p.get("subobject_id"),
|
||
ext_key=p.get("ext_key"), actor=actor)
|
||
|
||
|
||
def api_list_refs(db, params):
|
||
tenant_id, _a, _adm, p = _ctx(params)
|
||
return R.list_refs(db, tenant_id, blueprint_id=p.get("blueprint_id") or p.get("bp_id"),
|
||
ref_domain=p.get("ref_domain"), ref_table=p.get("ref_table"),
|
||
ref_id=p.get("ref_id"), src_kind=p.get("src_kind"),
|
||
src_id=p.get("src_id"), rel_type=p.get("rel_type"),
|
||
resolve_status=p.get("resolve_status"), version_id=p.get("version_id"),
|
||
limit=int(p.get("limit") or 500))
|
||
|
||
|
||
def api_add_refs(db, params):
|
||
tenant_id, actor, _adm, p = _ctx(params)
|
||
bp = p.get("blueprint_id") or p.get("bp_id")
|
||
refs = p.get("refs")
|
||
if isinstance(refs, list) and refs:
|
||
return R.bulk_add_refs(db, tenant_id, bp, refs, actor=actor)
|
||
payload = dict(p.get("ref") or p)
|
||
for k in ("tenant_id", "blueprint_id", "bp_id", "actor", "refs", "ref"):
|
||
payload.pop(k, None)
|
||
return R.add_ref(db, tenant_id, bp, payload, actor=actor)
|
||
|
||
|
||
def api_remove_ref(db, params):
|
||
tenant_id, actor, _adm, p = _ctx(params)
|
||
return R.remove_ref(db, tenant_id, p.get("ref_id") or p.get("id"), actor=actor)
|
||
|
||
|
||
def api_resolve_refs(db, params):
|
||
tenant_id, actor, _adm, p = _ctx(params)
|
||
return R.resolve_refs(db, tenant_id, blueprint_id=p.get("blueprint_id") or p.get("bp_id"),
|
||
actor=actor, reader=p.get("reader") if callable(p.get("reader")) else None)
|
||
|
||
|
||
def api_sync_refs(db, params):
|
||
tenant_id, actor, _adm, p = _ctx(params)
|
||
content = p.get("content")
|
||
if content is None:
|
||
content = (p.get("blueprint") or {}).get("content")
|
||
return R.sync_refs_from_content(db, tenant_id, p.get("blueprint_id") or p.get("bp_id"),
|
||
content, actor=actor, version_id=p.get("version_id"))
|
||
|
||
|
||
def api_ref_impact(db, params):
|
||
tenant_id, _a, _adm, p = _ctx(params)
|
||
return R.impact_of(db, tenant_id, p.get("ref_domain"), p.get("ref_table"), p.get("ref_id"))
|
||
|
||
|
||
# --------------------------------------------------------------------------
|
||
# 路由表 + 分发
|
||
# --------------------------------------------------------------------------
|
||
M1B_ROUTES = [
|
||
("GET", "/pbl/templates", api_list_templates),
|
||
("POST", "/pbl/templates", api_create_template),
|
||
("GET", "/pbl/templates/{template_id}", api_get_template),
|
||
("PUT", "/pbl/templates/{template_id}", api_update_template),
|
||
("DELETE", "/pbl/templates/{template_id}", api_delete_template),
|
||
("POST", "/pbl/templates/{template_id}/publish", api_publish_template),
|
||
("POST", "/pbl/templates/{template_id}/deprecate", api_deprecate_template),
|
||
("POST", "/pbl/templates/{template_id}/fork", api_fork_template),
|
||
("POST", "/pbl/templates/{template_id}/instantiate", api_instantiate_template),
|
||
("GET", "/pbl/ext-defs", api_list_ext_defs),
|
||
("POST", "/pbl/ext-defs", api_create_ext_def),
|
||
("PUT", "/pbl/ext-defs/{def_id}", api_update_ext_def),
|
||
("DELETE", "/pbl/ext-defs/{def_id}", api_delete_ext_def),
|
||
("GET", "/pbl/blueprints/{blueprint_id}/ext", api_get_blueprint_ext),
|
||
("PUT", "/pbl/blueprints/{blueprint_id}/ext", api_set_blueprint_ext),
|
||
("DELETE", "/pbl/blueprints/{blueprint_id}/ext", api_delete_blueprint_ext),
|
||
("GET", "/pbl/blueprints/{blueprint_id}/refs", api_list_refs),
|
||
("POST", "/pbl/blueprints/{blueprint_id}/refs", api_add_refs),
|
||
("POST", "/pbl/blueprints/{blueprint_id}/refs/resolve", api_resolve_refs),
|
||
("POST", "/pbl/blueprints/{blueprint_id}/refs/sync", api_sync_refs),
|
||
("DELETE", "/pbl/refs/{ref_id}", api_remove_ref),
|
||
("GET", "/pbl/refs/impact", api_ref_impact),
|
||
]
|
||
|
||
|
||
def dispatch(db, method, path, params=None):
|
||
"""轻量分发(供应用层/测试直接调用)。未命中 → 404 结构。"""
|
||
method = (method or "GET").upper()
|
||
path = (path or "").split("?")[0].rstrip("/") or "/"
|
||
for m, pat, fn in M1B_ROUTES:
|
||
if m != method:
|
||
continue
|
||
pseg, rseg = pat.rstrip("/").split("/")[1:], path.split("/")[1:]
|
||
if len(pseg) != len(rseg):
|
||
continue
|
||
args = {}
|
||
matched = True
|
||
for a, b in zip(pseg, rseg):
|
||
if a.startswith("{") and a.endswith("}"):
|
||
args[a[1:-1]] = b
|
||
elif a != b:
|
||
matched = False
|
||
break
|
||
if not matched:
|
||
continue
|
||
merged = dict(params or {})
|
||
merged.update(args)
|
||
try:
|
||
return fn(db, merged)
|
||
except PblM1bError as e:
|
||
return {"ok": False, "code": e.code, "message": e.message,
|
||
"detail": e.detail, "http_status": e.http_status}
|
||
return {"ok": False, "code": "PBL_ROUTE_NOT_FOUND", "message": "no route: %s %s" % (method, path),
|
||
"http_status": 404}
|
||
|
||
|
||
def register_m1b_routes(app=None, register=None):
|
||
"""把 M1B_ROUTES 注册到应用(init.py 调用)。register(method, path, handler) 由应用层提供。"""
|
||
if not callable(register):
|
||
return {"ok": False, "registered": 0, "reason": "register callback not provided"}
|
||
n = 0
|
||
for m, pat, fn in M1B_ROUTES:
|
||
try:
|
||
register(m, pat, fn)
|
||
n += 1
|
||
except Exception:
|
||
continue
|
||
return {"ok": True, "registered": n}
|