feat: CRUD auto-generates validation code from alters.rules

- extract_validation_rules() extracts rules from browserfields.alters
- data_new_tmpl: adds runtime validation before DB insert
- data_update_tmpl: adds runtime validation before DB update
- Supports: required, minlength, maxlength, min, max, pattern, email, number
- Backend returns Error widget with validation messages on failure
This commit is contained in:
Hermes Agent 2026-06-18 17:22:12 +08:00
parent ebd4b4a99e
commit 3f8a0ce876
15 changed files with 118 additions and 2 deletions

View File

@ -1,8 +1,12 @@
Metadata-Version: 2.4 Metadata-Version: 2.1
Name: xls2ddl Name: xls2ddl
Version: 1.1.3 Version: 1.1.3
Summary: a xlsx file to database ddl converter Summary: a xlsx file to database ddl converter
Home-page: UNKNOWN
Author: "yu moqing" Author: "yu moqing"
Author-email: "yumoqing@gmail.com" Author-email: "yumoqing@gmail.com"
License: "MIT" License: "MIT"
Requires-Dist: apppublic Platform: UNKNOWN
UNKNOWN

View File

@ -3,3 +3,4 @@ json2ddl = xls2ddl.json2ddl:main
json2xlsx = xls2ddl.json2xlsx:main json2xlsx = xls2ddl.json2xlsx:main
xls2ddl = xls2ddl.xls2ddl:main xls2ddl = xls2ddl.xls2ddl:main
xls2ui = xls2ddl.xls2ui:main xls2ui = xls2ddl.xls2ui:main

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -244,6 +244,54 @@ if not userorgid:
} }
ns['{{logined_userorgid}}'] = userorgid ns['{{logined_userorgid}}'] = userorgid
{% endif %} {% endif %}
{% if validation_rules %}
_validation_rules = json.loads(r'''{{validation_rules}}''')
import re as _re
_errors = []
for _fname, _rules in _validation_rules.items():
_val = params_kw.get(_fname, '')
if _val is None: _val = ''
_val = str(_val)
for _rule in _rules:
_rt = _rule.get('type', '')
_rm = _rule.get('message', _fname)
_rv = _rule.get('value')
if _rt == 'required':
if not _val or _val.strip() == '':
_errors.append(_rm)
break
elif _rt == 'minlength':
if _val and len(_val) < int(_rv):
_errors.append(_rm)
break
elif _rt == 'maxlength':
if len(_val) > int(_rv):
_errors.append(_rm)
break
elif _rt in ('min', 'max'):
if _val:
try:
_n = float(_val)
if _rt == 'min' and _n < float(_rv): _errors.append(_rm); break
if _rt == 'max' and _n > float(_rv): _errors.append(_rm); break
except (ValueError, TypeError):
_errors.append(_rm)
break
elif _rt == 'pattern':
if _val and not _re.match(_rv, _val):
_errors.append(_rm)
break
elif _rt == 'email':
if _val and not _re.match(r'^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$', _val):
_errors.append(_rm)
break
elif _rt == 'number':
if _val:
try: float(_val)
except (ValueError, TypeError): _errors.append(_rm); break
if _errors:
return {"widgettype":"Error","options":{"title":"Validation Failed","cwidth":16,"cheight":9,"timeout":3,"message":"; ".join(_errors)}}
{% endif %}
db = DBPools() db = DBPools()
dbname = get_module_dbname('{{modulename}}') dbname = get_module_dbname('{{modulename}}')
async with db.sqlorContext(dbname) as sor: async with db.sqlorContext(dbname) as sor:
@ -309,6 +357,54 @@ ns['{{logined_userorgid}}'] = userorgid
if params_kw.get('{{f}}'): if params_kw.get('{{f}}'):
ns['{{f}}'] = password_encode(params_kw.get('{{f}}')) ns['{{f}}'] = password_encode(params_kw.get('{{f}}'))
{% endfor %} {% endfor %}
{% if validation_rules %}
_validation_rules = json.loads(r'''{{validation_rules}}''')
import re as _re
_errors = []
for _fname, _rules in _validation_rules.items():
\t_val = params_kw.get(_fname, '')
\tif _val is None: _val = ''
\t_val = str(_val)
\tfor _rule in _rules:
\t\t_rt = _rule.get('type', '')
\t\t_rm = _rule.get('message', _fname)
\t\t_rv = _rule.get('value')
\t\tif _rt == 'required':
\t\t\tif not _val or _val.strip() == '':
\t\t\t\t_errors.append(_rm)
\t\t\t\tbreak
\t\telif _rt == 'minlength':
\t\t\tif _val and len(_val) < int(_rv):
\t\t\t\t_errors.append(_rm)
\t\t\t\tbreak
\t\telif _rt == 'maxlength':
\t\t\tif len(_val) > int(_rv):
\t\t\t\t_errors.append(_rm)
\t\t\t\tbreak
\t\telif _rt in ('min', 'max'):
\t\t\tif _val:
\t\t\t\ttry:
\t\t\t\t\t_n = float(_val)
\t\t\t\t\tif _rt == 'min' and _n < float(_rv): _errors.append(_rm); break
\t\t\t\t\tif _rt == 'max' and _n > float(_rv): _errors.append(_rm); break
\t\t\t\texcept (ValueError, TypeError):
\t\t\t\t\t_errors.append(_rm)
\t\t\t\t\tbreak
\t\telif _rt == 'pattern':
\t\t\tif _val and not _re.match(_rv, _val):
\t\t\t\t_errors.append(_rm)
\t\t\t\tbreak
\t\telif _rt == 'email':
\t\t\tif _val and not _re.match(r'^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$', _val):
\t\t\t\t_errors.append(_rm)
\t\t\t\tbreak
\t\telif _rt == 'number':
\t\t\tif _val:
\t\t\t\ttry: float(_val)
\t\t\t\texcept (ValueError, TypeError): _errors.append(_rm); break
if _errors:
\treturn {"widgettype":"Error","options":{"title":"Validation Failed","cwidth":16,"cheight":9,"timeout":3,"message":"; ".join(_errors)}}
{% endif %}
db = DBPools() db = DBPools()
dbname = get_module_dbname('{{modulename}}') dbname = get_module_dbname('{{modulename}}')

View File

@ -263,9 +263,23 @@ def build_data_browser(pat: str, desc: dict):
with codecs.open(os.path.join(pat, f'index.ui'), 'w', "utf-8") as f: with codecs.open(os.path.join(pat, f'index.ui'), 'w', "utf-8") as f:
f.write(filter_backslash(s)) f.write(filter_backslash(s))
def extract_validation_rules(desc) -> str:
"""Extract validation rules from field alters. Returns JSON string or empty."""
rules = {}
alters = desc.get('browserfields', {}).get('alters', {})
if not alters:
return ''
for fname, alter in alters.items():
if 'rules' in alter and alter['rules']:
rules[fname] = alter['rules']
if not rules:
return ''
return json.dumps(rules, ensure_ascii=False)
def build_data_new(pat: str, desc: dict): def build_data_new(pat: str, desc: dict):
e = MyTemplateEngine([]) e = MyTemplateEngine([])
desc = desc.copy() desc = desc.copy()
desc['validation_rules'] = extract_validation_rules(desc)
s = e.renders(data_new_tmpl, desc) s = e.renders(data_new_tmpl, desc)
with codecs.open(os.path.join(pat, f'add_{desc.tblname}.dspy'), 'w', "utf-8") as f: with codecs.open(os.path.join(pat, f'add_{desc.tblname}.dspy'), 'w', "utf-8") as f:
f.write(filter_backslash(s)) f.write(filter_backslash(s))
@ -273,6 +287,7 @@ def build_data_new(pat: str, desc: dict):
def build_data_update(pat: str, desc: dict): def build_data_update(pat: str, desc: dict):
e = MyTemplateEngine([]) e = MyTemplateEngine([])
desc = desc.copy() desc = desc.copy()
desc['validation_rules'] = extract_validation_rules(desc)
s = e.renders(data_update_tmpl, desc) s = e.renders(data_update_tmpl, desc)
with codecs.open(os.path.join(pat, f'update_{desc.tblname}.dspy'), 'w', "utf-8") as f: with codecs.open(os.path.join(pat, f'update_{desc.tblname}.dspy'), 'w', "utf-8") as f:
f.write(filter_backslash(s)) f.write(filter_backslash(s))