#!/usr/bin/env python3 # -*- coding: utf-8 -*- """CRUD 端点存在性自检(QC 硬门禁 #2 的 CI 化)。 json/*.json 里 `params.editable` / `new|update|delete_data_url` / `browserfields.alters[].dataurl` 引用的每个 `../api/xxx.dspy`,都必须在 `wwwroot/api/xxx.dspy` 真实落盘 —— 少一个文件,前端点了就是 404。 用法: python3 scripts/check_crud_endpoints.py # 只核对手写 api/ 端点 python3 scripts/check_crud_endpoints.py --generated # 连 xls2ui 生成物一起核(部署机用) 退出码:0=全部命中;1=存在缺失端点。不连库、不依赖第三方包,可直接放 CI。 """ import argparse import io import json import os import re import sys ROOT = os.path.normpath(os.path.join(os.path.dirname(os.path.abspath(__file__)), '..')) JSON_DIR = os.path.join(ROOT, 'json') API_DIR = os.path.join(ROOT, 'wwwroot', 'api') # 匹配 ../api/xxx.dspy 与 /pbl_evidence/api/xxx.dspy 两种写法 URL_RE = re.compile(r'api/([A-Za-z0-9_]+\.dspy)') def referenced_endpoints(): """返回 {dspy 文件名: [引用它的 json 文件, ...]}。""" refs = {} if not os.path.isdir(JSON_DIR): return refs for fn in sorted(os.listdir(JSON_DIR)): if not fn.endswith('.json'): continue path = os.path.join(JSON_DIR, fn) try: json.loads(io.open(path, encoding='utf-8').read()) except Exception as exc: refs.setdefault('__INVALID_JSON__:%s' % fn, []).append(str(exc)) continue text = io.open(path, encoding='utf-8').read() for name in URL_RE.findall(text): refs.setdefault(name, []).append('json/' + fn) return refs def generated_endpoints(): """xls2ui 为每个 CRUD 定义生成 wwwroot/{alias}/{get,add,update,delete}_{alias}.dspy。 注意落点是 **wwwroot/{alias}/** 子目录(不是 wwwroot/api/)——那是构建产物目录, 开发仓库被 .gitignore 排除,只有部署机跑过 build.sh 后才存在。 """ out = [] for fn in sorted(os.listdir(JSON_DIR)): if not fn.endswith('.json'): continue d = json.loads(io.open(os.path.join(JSON_DIR, fn), encoding='utf-8').read()) alias = d.get('alias') or d.get('tblname') if not alias: continue for op in ('get', 'add', 'update', 'delete'): out.append(os.path.join(alias, '%s_%s.dspy' % (op, alias))) out.append(os.path.join(alias, 'index.ui')) return out def main(): ap = argparse.ArgumentParser(description='verify CRUD-referenced .dspy endpoints exist') ap.add_argument('--generated', action='store_true', help='also check xls2ui-generated get_/add_/update_/delete_{alias}.dspy ' '(build artifacts; run on the deployment host after build.sh)') args = ap.parse_args() refs = referenced_endpoints() bad = [] for name, srcs in sorted(refs.items()): if name.startswith('__INVALID_JSON__:'): bad.append('%s 不是合法 JSON:%s' % (name, srcs[0])) continue if not os.path.exists(os.path.join(API_DIR, name)): bad.append('%s 缺失(被 %s 引用)' % (name, ', '.join(srcs))) gen_missing = [] if args.generated: for name in generated_endpoints(): if not os.path.exists(os.path.join(ROOT, 'wwwroot', name)): gen_missing.append(name) print('[pbl_evidence] json/ 引用的端点: %d' % len(refs)) for line in bad: print('MISSING ' + line) if args.generated: print('[pbl_evidence] xls2ui 生成物端点: %d' % len(generated_endpoints())) for name in gen_missing: print('MISSING(gen) wwwroot/' + name) ok = not bad and not gen_missing print('[pbl_evidence] crud endpoints: %s' % ('PASS' if ok else 'FAIL')) return 0 if ok else 1 if __name__ == '__main__': sys.exit(main())