pbl_evidence/scripts/check_crud_json.py
2026-09-22 12:08:18 +08:00

131 lines
6.1 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""CRUD 定义json/*.json规范自检QC 退回意见 #4/#5/#6 的 CI 化)。
三条硬规则(依据 crud-definition-spec / module-development-spec
1. **合法键**`params` 只允许规范定义的键。`order_by` / `page_size` 是**自创键**——
排序用 `sortby`(数组),分页由 sqlor `sqlPaging()` 用 ns 的 `page`/`rows` 处理,
在 CRUD JSON 里硬编码 ORDER BY / 页大小属禁项(且与 sortby 语义冲突)。
2. **alias 唯一**alias 只用于「同一张表的多个逻辑视图」。两张**不同**的表共用同一
aliasxls2ui 会生成到同一个 `wwwroot/{alias}/` 目录互相覆盖。
3. **文件名**`json/{table_name}.json` 或 `{alias}.json`。文件名与 tblname/alias 都不
匹配时,人和工具都对不上号(曾出现 evidence_pbl_artifact.json 这种两头不靠的命名)。
退出码 0=PASS1=FAIL。不连库、无第三方依赖可直接放 CI。
"""
import io
import json
import os
import sys
ROOT = os.path.normpath(os.path.join(os.path.dirname(os.path.abspath(__file__)), '..'))
JSON_DIR = os.path.join(ROOT, 'json')
# crud-definition-spec 中 list/tree 视图 params 的合法键集合
ALLOWED_PARAM_KEYS = {
'sortby', 'logined_userorgid', 'logined_userid', 'data_filter',
'confidential_fields', 'editor', 'browserfields', 'editexclouded',
'edit_exclouded_fields', 'subtables', 'idField', 'textField',
'parentField', 'editable', 'uitype',
# editable 三 URL 规范上要求「params 顶层」xls2ddl 从顶层读),同时兼容
# 旧写法嵌在 editable 内 —— 两处都允许,但顶层必须存在(见 TOP_LEVEL_URL_KEYS
'new_data_url', 'update_data_url', 'delete_data_url',
# 展示/检索辅助键(规范内已文档化的可选项)
'filter_labels', 'filter_label', 'filter_title', 'data_url',
}
FORBIDDEN_PARAM_KEYS = {'order_by', 'page_size', 'page', 'rows', 'limit', 'offset',
'fields', 'joins', 'select_fields', 'grid', 'form', 'components'}
TOP_LEVEL_URL_KEYS = ('new_data_url', 'update_data_url', 'delete_data_url')
def main():
problems = []
alias_map = {}
if not os.path.isdir(JSON_DIR):
print('[pbl_evidence] no json/ directory')
return 1
for fn in sorted(os.listdir(JSON_DIR)):
if not fn.endswith('.json'):
continue
rel = 'json/' + fn
path = os.path.join(JSON_DIR, fn)
try:
d = json.loads(io.open(path, encoding='utf-8').read())
except Exception as exc:
problems.append('%s 不是合法 JSON%s' % (rel, exc))
continue
tbl = d.get('tblname')
if not tbl:
problems.append('%s 缺 tblname禁 tablename/name/type' % rel)
alias = d.get('alias')
if alias:
alias_map.setdefault(alias, set()).add(tbl or rel)
# 规则 3文件名必须等于 {tblname}.json 或 {alias}.json
stem = fn[:-5]
allowed_names = {tbl, alias} - {None}
# 多逻辑视图约定 alias = {table}_{view},故也接受以 tbl 开头的下划线名
if stem not in allowed_names and not any(
stem == t or stem.startswith(t + '_') for t in allowed_names):
problems.append('%s 文件名与 tblname=%s / alias=%s 均不匹配'
'(应为 {tblname}.json 或 {alias}.json' % (rel, tbl, alias))
params = d.get('params')
if not isinstance(params, dict) or not params:
problems.append('%s 缺 params 或为空' % rel)
continue
# 规则 1合法键 / 禁键
for key in params:
if key in FORBIDDEN_PARAM_KEYS:
problems.append('%s params.%s 是自创/禁用键(排序用 sortby 数组,'
'分页由 sqlor sqlPaging 用 ns.page/rows 处理)' % (rel, key))
elif key not in ALLOWED_PARAM_KEYS:
problems.append('%s params.%s 不在 crud-definition-spec 合法键内' % (rel, key))
# sortby 必须是数组
if 'sortby' in params and not isinstance(params['sortby'], list):
problems.append('%s params.sortby 必须是数组' % rel)
# editable顶层三 URL 必须齐xls2ddl 从 params 顶层读,嵌套写法会被静默忽略)
for key in TOP_LEVEL_URL_KEYS:
if key not in params:
problems.append('%s params 顶层缺 %s(嵌套在 editable 内会被 xls2ui 静默忽略,'
'生成默认 add_{table}.dspy' % (rel, key))
ed = params.get('editable')
if isinstance(ed, dict):
for key in TOP_LEVEL_URL_KEYS:
if key not in ed:
problems.append('%s params.editable 缺 %s' % (rel, key))
# browserfields.alters 的 uitype=code 项必须有 data 或 dataurl
bf = params.get('browserfields') or {}
for fname, spec in (bf.get('alters') or {}).items():
if not isinstance(spec, dict):
problems.append('%s alters.%s 不是对象' % (rel, fname))
continue
if spec.get('uitype') != 'code':
problems.append('%s alters.%s uitype 应为 "code"' % (rel, fname))
if not spec.get('data') and not spec.get('dataurl'):
problems.append('%s alters.%s 既无 data 也无 dataurl' % (rel, fname))
if spec.get('data_field'):
problems.append('%s alters.%s 用了已废弃的 data_field' % (rel, fname))
# 规则 2alias 不得被两张不同的表共用
for alias, tbls in sorted(alias_map.items()):
if len(tbls) > 1:
problems.append('alias "%s" 被多张表共用 %s —— xls2ui 会生成到同一 '
'wwwroot/%s/ 目录互相覆盖' % (alias, sorted(tbls), alias))
for line in problems:
print('FAIL ' + line)
print('[pbl_evidence] crud json: %s (%d 项问题)'
% ('PASS' if not problems else 'FAIL', len(problems)))
return 0 if not problems else 1
if __name__ == '__main__':
sys.exit(main())