167 lines
6.5 KiB
Python
167 lines
6.5 KiB
Python
#!/usr/bin/env python3
|
||
# -*- coding: utf-8 -*-
|
||
"""pbl_blueprint 派生物生成器(幂等)。
|
||
|
||
输入:pbl_blueprint/models/{tblname}.json —— 四段式表定义(summary/fields/indexes/codes)
|
||
输出:
|
||
1) pbl_blueprint/json/{tblname}.json —— CRUD 契约定义(根键 = tblname + params(editable/browserfields))
|
||
2) pbl_blueprint/sql/pbl_blueprint.core.sql —— DDL 派生物(engine=postgresql,禁止手工维护)
|
||
|
||
用法:python3 scripts/derive_artifacts.py
|
||
QC 核对点:json/*.json 与 sql/*.sql 均由本脚本机械派生,改表只需改 models/*.json 后重跑本脚本。
|
||
"""
|
||
import json
|
||
import os
|
||
import glob
|
||
|
||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||
PKG = os.path.join(os.path.dirname(HERE), 'pbl_blueprint')
|
||
MODELS_DIR = os.path.join(PKG, 'models')
|
||
JSON_DIR = os.path.join(PKG, 'json')
|
||
SQL_DIR = os.path.join(PKG, 'sql')
|
||
|
||
# 表 -> dspy 接口前缀(CRUD editable url 用)
|
||
API_PREFIX = {
|
||
'pbl_blueprint': 'blueprint',
|
||
'pbl_blueprint_version': 'version',
|
||
'pbl_blueprint_template': 'template',
|
||
'pbl_blueprint_template_item': 'template_item',
|
||
}
|
||
BROWSER_FIELDS = ('id', 'tenant_id', 'class_id', 'code', 'name', 'blueprint_code',
|
||
'blueprint_name', 'template_code', 'template_name', 'status',
|
||
'version_status', 'version_no', 'subobject_type', 'domain_code',
|
||
'grade_code', 'subject_code', 'complexity_level', 'seq_no',
|
||
'created_at', 'updated_at')
|
||
HEAVY_FIELDS = ('snapshot_json', 'content_json', 'item_payload_json',
|
||
'quality_criteria', 'knowledge_points', 'tags_json')
|
||
IMMUTABLE = ('id', 'tenant_id', 'created_by', 'created_at')
|
||
|
||
|
||
def api_prefix(tbl):
|
||
if tbl in API_PREFIX:
|
||
return API_PREFIX[tbl]
|
||
return 'subobject'
|
||
|
||
|
||
def load_models():
|
||
docs = {}
|
||
for p in sorted(glob.glob(os.path.join(MODELS_DIR, '*.json'))):
|
||
tbl = os.path.basename(p)[:-5]
|
||
doc = json.load(open(p, encoding='utf-8'))
|
||
for seg in ('summary', 'fields', 'indexes', 'codes'):
|
||
if seg not in doc:
|
||
raise SystemExit('models/%s.json 缺四段式必需段: %s' % (tbl, seg))
|
||
if not isinstance(doc['fields'], dict):
|
||
raise SystemExit('models/%s.json fields 必须是对象(列名->定义)' % tbl)
|
||
docs[tbl] = doc
|
||
return docs
|
||
|
||
|
||
def build_crud(tbl, doc):
|
||
fields = doc['fields']
|
||
names = list(fields.keys())
|
||
editable = [n for n in names if n not in IMMUTABLE]
|
||
browser = [n for n in names if n in BROWSER_FIELDS]
|
||
alters = {}
|
||
for n, spec in fields.items():
|
||
if spec.get('appcode'):
|
||
alters[n] = {'uitype': 'code', 'appcode': spec['appcode'],
|
||
'label': spec.get('label', n)}
|
||
a = api_prefix(tbl)
|
||
return {tbl: {
|
||
'summary': doc['summary'].get('label', tbl),
|
||
'alias': tbl + '_crud',
|
||
'params': {
|
||
'sortby': 'created_at',
|
||
'sortorder': 'desc',
|
||
'tenant_scoped': True,
|
||
'browserfields': {
|
||
'fields': browser,
|
||
'alters': alters,
|
||
'exclouded': list(HEAVY_FIELDS),
|
||
},
|
||
'editable': {
|
||
'new_data_url': "{{entire_url('../api/%s_save.dspy')}}" % a,
|
||
'update_data_url': "{{entire_url('../api/%s_save.dspy')}}" % a,
|
||
'delete_data_url': "{{entire_url('../api/%s_delete.dspy')}}" % a,
|
||
'list_data_url': "{{entire_url('../api/%s_list.dspy')}}" % a,
|
||
'get_data_url': "{{entire_url('../api/%s_get.dspy')}}" % a,
|
||
'editexclouded': list(IMMUTABLE),
|
||
},
|
||
'required': [n for n, s in fields.items() if s.get('required')],
|
||
'data_filter': {'AND': [{'field': 'tenant_id', 'op': '=', 'var': 'tenant_id'}]},
|
||
},
|
||
'codes': doc['codes'],
|
||
'table_meta': {
|
||
'pk': 'id',
|
||
'tenant_field': 'tenant_id',
|
||
'soft_delete_field': 'is_deleted',
|
||
'engine': doc['summary'].get('engine', 'postgresql'),
|
||
'indexes': list(doc['indexes'].keys()),
|
||
},
|
||
}}
|
||
|
||
|
||
def col_ddl(name, spec):
|
||
ty = spec['type']
|
||
if ty.startswith('str'):
|
||
return '%s varchar(%s)' % (name, ty[ty.index('(') + 1:ty.index(')')])
|
||
if ty == 'text':
|
||
return '%s text' % name
|
||
if ty == 'int':
|
||
return '%s integer' % name
|
||
if ty.startswith('double'):
|
||
return '%s numeric(18,2)' % name
|
||
if ty == 'datetime':
|
||
return '%s timestamp' % name
|
||
raise SystemExit('未知抽象类型: %s (%s)' % (ty, name))
|
||
|
||
|
||
def build_sql(docs):
|
||
lines = [
|
||
'-- pbl_blueprint 核心表 DDL',
|
||
'-- 本文件由 scripts/derive_artifacts.py 从 pbl_blueprint/models/*.json 机械派生,禁止手工编辑',
|
||
'-- engine: postgresql(与 projects/pbls/env/*.json 的 db.engine 一致)',
|
||
'-- 主键 id 为应用层生成的 str(32):不使用 SERIAL / BIGSERIAL / nextval / AUTO_INCREMENT',
|
||
'',
|
||
]
|
||
for tbl in sorted(docs.keys()):
|
||
doc = docs[tbl]
|
||
fields = doc['fields']
|
||
lines.append('-- %s: %s' % (tbl, doc['summary'].get('label', '')))
|
||
cols = ',\n'.join(
|
||
' ' + col_ddl(k, v) + (' NOT NULL' if (v.get('required') or k == 'id') else '')
|
||
for k, v in fields.items())
|
||
lines.append('CREATE TABLE IF NOT EXISTS %s (' % tbl)
|
||
lines.append(cols)
|
||
lines.append(');')
|
||
for k, v in fields.items():
|
||
lines.append("COMMENT ON COLUMN %s.%s IS '%s';" % (tbl, k, v.get('label', '')))
|
||
lines.append('')
|
||
for iname, isp in doc['indexes'].items():
|
||
if iname == 'primary':
|
||
continue
|
||
uniq = 'UNIQUE ' if isp.get('unique') else ''
|
||
lines.append('CREATE %sINDEX IF NOT EXISTS %s_%s ON %s (%s);' %
|
||
(uniq, tbl, iname, tbl, ', '.join(isp['fields'])))
|
||
lines.append('')
|
||
return '\n'.join(lines)
|
||
|
||
|
||
def main():
|
||
docs = load_models()
|
||
os.makedirs(JSON_DIR, exist_ok=True)
|
||
os.makedirs(SQL_DIR, exist_ok=True)
|
||
for tbl, doc in docs.items():
|
||
out = os.path.join(JSON_DIR, '%s.json' % tbl)
|
||
with open(out, 'w', encoding='utf-8') as fp:
|
||
json.dump(build_crud(tbl, doc), fp, ensure_ascii=False, indent=2)
|
||
with open(os.path.join(SQL_DIR, 'pbl_blueprint.core.sql'), 'w', encoding='utf-8') as fp:
|
||
fp.write(build_sql(docs))
|
||
print('models=%d json=%d sql_tables=%d' %
|
||
(len(docs), len(glob.glob(os.path.join(JSON_DIR, '*.json'))), len(docs)))
|
||
|
||
|
||
if __name__ == '__main__':
|
||
main()
|