From acb96743753ff5256ec450a228898d3532ac5f65 Mon Sep 17 00:00:00 2001 From: Hermes Agent Date: Wed, 20 May 2026 18:28:59 +0800 Subject: [PATCH] feat: CRUD definitions, build script, DDL generation - 3 CRUD JSON files: customer_balance, accounting_records, sync_state - Build script with model validation, CRUD validation, DDL generation - DDL: db/schema.sql (72 lines, 7 tables) - Scripts: validate_models.py, validate_crud.py, generate_ddl.py --- build.sh | 21 ++++- db/schema.sql | 152 +++++++++++++++++++++++++++++++++++ json/accounting_records.json | 41 ++++++++++ json/customer_balance.json | 33 ++++++++ json/sync_state.json | 36 +++++++++ scripts/generate_ddl.py | 55 +++++++++++++ scripts/validate_crud.py | 32 ++++++++ scripts/validate_models.py | 41 ++++++++++ 8 files changed, 410 insertions(+), 1 deletion(-) create mode 100644 db/schema.sql create mode 100644 json/accounting_records.json create mode 100644 json/customer_balance.json create mode 100644 json/sync_state.json create mode 100755 scripts/generate_ddl.py create mode 100755 scripts/validate_crud.py create mode 100755 scripts/validate_models.py diff --git a/build.sh b/build.sh index 187a4f1..63a8f06 100755 --- a/build.sh +++ b/build.sh @@ -1,3 +1,22 @@ #!/bin/bash +# SageAPI build script +set -e -xls2ui -m ../models -o ../wwwroot sageapi *.json +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +cd "$SCRIPT_DIR" + +echo "=== SageAPI Build ===" + +# 1. Validate JSON models +echo "[1/3] Validating model definitions..." +python3 scripts/validate_models.py + +# 2. Validate CRUD JSON +echo "[2/3] Validating CRUD definitions..." +python3 scripts/validate_crud.py + +# 3. Generate DDL +echo "[3/3] Generating DDL..." +python3 scripts/generate_ddl.py + +echo "=== Build Complete ===" diff --git a/db/schema.sql b/db/schema.sql new file mode 100644 index 0000000..8c19ae7 --- /dev/null +++ b/db/schema.sql @@ -0,0 +1,152 @@ +-- SageAPI DDL (auto-generated) + +CREATE TABLE IF NOT EXISTS `accounting_records` ( +`id` VARCHAR(32) NOT NULL DEFAULT COMMENT '主键', +`customer_id` VARCHAR(32) NOT NULL DEFAULT COMMENT '客户ID', +`llmid` VARCHAR(32) NULL DEFAULT COMMENT '模型ID', +`model_name` VARCHAR(128) NULL DEFAULT COMMENT '模型名称', +`pricing_id` VARCHAR(32) NULL DEFAULT COMMENT '定价ID', +`input_tokens` BIGINT NULL COMMENT '输入token数', +`output_tokens` BIGINT NULL COMMENT '输出token数', +`total_tokens` BIGINT NULL COMMENT '总token数', +`quantity` DECIMAL(15,4) NULL COMMENT '用量(图片数/分钟数等)', +`amount` DECIMAL(15,6) NOT NULL DEFAULT 0.0 COMMENT '金额', +`currency` VARCHAR(8) NOT NULL DEFAULT 'CNY' COMMENT '货币单位', +`request_id` VARCHAR(64) NULL DEFAULT COMMENT '请求ID(幂等键)', +`transno` VARCHAR(64) NULL DEFAULT COMMENT '事务号', +`status` VARCHAR(16) NOT NULL DEFAULT 'pending' COMMENT '状态: pending/accounted/failed', +`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', +`updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '更新时间', + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +ALTER TABLE `accounting_records` ADD INDEX `idx_customer_id` (`customer_id`); + +ALTER TABLE `accounting_records` ADD INDEX `idx_llmid` (`llmid`); + +ALTER TABLE `accounting_records` ADD UNIQUE `idx_request_id` (`request_id`); + +ALTER TABLE `accounting_records` ADD INDEX `idx_status` (`status`); + +ALTER TABLE `accounting_records` ADD INDEX `idx_created_at` (`created_at`); + +CREATE TABLE IF NOT EXISTS `customer_balance` ( +`id` VARCHAR(32) NOT NULL DEFAULT COMMENT '主键,即 customer_id', +`balance` DECIMAL(15,4) NOT NULL DEFAULT 0.0 COMMENT '当前余额', +`currency` VARCHAR(8) NOT NULL DEFAULT 'CNY' COMMENT '货币单位', +`credit_limit` DECIMAL(15,4) NULL COMMENT '信用额度', +`last_recharge` DATETIME NULL COMMENT '最后充值时间', +`last_consumption` DATETIME NULL COMMENT '最后消费时间', +`status` VARCHAR(16) NOT NULL DEFAULT 'active' COMMENT '状态: active/suspended/arrears', +`sync_version` VARCHAR(32) NULL DEFAULT COMMENT '同步版本号', +`cached_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '缓存更新时间', + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +ALTER TABLE `customer_balance` ADD INDEX `idx_status` (`status`); + +ALTER TABLE `customer_balance` ADD INDEX `idx_balance` (`balance`); + +CREATE TABLE IF NOT EXISTS `llmage_cache` ( +`id` VARCHAR(32) NOT NULL DEFAULT COMMENT '主键', +`llmid` VARCHAR(32) NOT NULL DEFAULT COMMENT '关联模型ID', +`model_name` VARCHAR(128) NULL DEFAULT COMMENT '模型名称', +`upappid` VARCHAR(32) NOT NULL DEFAULT COMMENT '上游应用ID', +`apiname` VARCHAR(128) NOT NULL DEFAULT COMMENT 'API名称', +`api_url` VARCHAR(512) NULL DEFAULT COMMENT 'API端点URL', +`api_params` TEXT NULL COMMENT 'API参数配置JSON', +`model_params` TEXT NULL COMMENT '模型参数配置JSON(max_tokens, temperature等)', +`status` VARCHAR(16) NOT NULL DEFAULT 'active' COMMENT '状态: active/inactive', +`sync_version` VARCHAR(32) NULL DEFAULT COMMENT '同步版本号', +`cached_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '缓存写入时间', + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +ALTER TABLE `llmage_cache` ADD INDEX `idx_llmid` (`llmid`); + +ALTER TABLE `llmage_cache` ADD INDEX `idx_upappid` (`upappid`); + +ALTER TABLE `llmage_cache` ADD INDEX `idx_apiname` (`apiname`); + +CREATE TABLE IF NOT EXISTS `pricing_cache` ( +`id` VARCHAR(32) NOT NULL DEFAULT COMMENT '主键,对应 pricing_program id (ppid)', +`llmid` VARCHAR(32) NOT NULL DEFAULT COMMENT '关联模型ID', +`model_name` VARCHAR(128) NULL DEFAULT COMMENT '模型名称', +`pricing_type` VARCHAR(32) NOT NULL DEFAULT COMMENT '计费类型: token/image/video/audio', +`input_price` DECIMAL(10,6) NULL COMMENT '输入单价(每千token)', +`output_price` DECIMAL(10,6) NULL COMMENT '输出单价(每千token)', +`unit_price` DECIMAL(10,6) NULL COMMENT '统一单价(按次/按图/按分钟等)', +`currency` VARCHAR(8) NOT NULL DEFAULT 'CNY' COMMENT '货币单位', +`status` VARCHAR(16) NOT NULL DEFAULT 'active' COMMENT '状态: active/inactive/deprecated', +`effective_from` DATETIME NULL COMMENT '生效时间', +`effective_to` DATETIME NULL COMMENT '失效时间', +`sync_version` VARCHAR(32) NULL DEFAULT COMMENT '同步版本号', +`cached_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '缓存写入时间', + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +ALTER TABLE `pricing_cache` ADD INDEX `idx_llmid` (`llmid`); + +ALTER TABLE `pricing_cache` ADD INDEX `idx_pricing_type` (`pricing_type`); + +ALTER TABLE `pricing_cache` ADD INDEX `idx_status` (`status`); + +CREATE TABLE IF NOT EXISTS `sync_state` ( +`id` VARCHAR(32) NOT NULL DEFAULT COMMENT '主键', +`entity_type` VARCHAR(32) NOT NULL DEFAULT COMMENT '实体类型: users/pricing/llmage/uapi', +`entity_id` VARCHAR(64) NULL DEFAULT COMMENT '实体标识(全量同步时为空)', +`last_sync_time` DATETIME NULL COMMENT '最后同步时间', +`sync_version` VARCHAR(32) NULL DEFAULT COMMENT 'Sage返回的版本标识', +`sync_status` VARCHAR(16) NOT NULL DEFAULT 'success' COMMENT '同步状态: success/pending/failed', +`error_msg` TEXT NULL COMMENT '失败原因', +`retry_count` INT NOT NULL DEFAULT 0 COMMENT '重试次数', +`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', +`updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '更新时间', + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +ALTER TABLE `sync_state` ADD INDEX `idx_entity_type` (`entity_type`); + +ALTER TABLE `sync_state` ADD UNIQUE `idx_entity_type_id` (`entity_type`,`entity_id`); + +ALTER TABLE `sync_state` ADD INDEX `idx_sync_status` (`sync_status`); + +CREATE TABLE IF NOT EXISTS `uapi_cache` ( +`id` VARCHAR(32) NOT NULL DEFAULT COMMENT '主键', +`upappid` VARCHAR(32) NOT NULL DEFAULT COMMENT '上游应用ID', +`apiname` VARCHAR(128) NOT NULL DEFAULT COMMENT 'API名称', +`method` VARCHAR(16) NULL DEFAULT 'POST' COMMENT 'HTTP方法', +`endpoint` VARCHAR(512) NULL DEFAULT COMMENT 'API端点', +`auth_type` VARCHAR(32) NULL DEFAULT 'bearer' COMMENT '认证类型', +`rate_limit` INT NULL COMMENT '速率限制(次/分钟)', +`description` TEXT NULL COMMENT 'API描述', +`status` VARCHAR(16) NOT NULL DEFAULT 'active' COMMENT '状态', +`sync_version` VARCHAR(32) NULL DEFAULT COMMENT '同步版本号', +`cached_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '缓存写入时间', + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +ALTER TABLE `uapi_cache` ADD UNIQUE `idx_upappid_apiname` (`upappid`,`apiname`); + +ALTER TABLE `uapi_cache` ADD INDEX `idx_status` (`status`); + +CREATE TABLE IF NOT EXISTS `users_cache` ( +`id` VARCHAR(32) NOT NULL DEFAULT COMMENT '主键,对应 users 表 id', +`username` VARCHAR(128) NOT NULL DEFAULT COMMENT '用户名', +`orgid` VARCHAR(32) NULL DEFAULT COMMENT '组织ID', +`orgname` VARCHAR(255) NULL DEFAULT COMMENT '组织名称', +`email` VARCHAR(128) NULL DEFAULT COMMENT '邮箱', +`phone` VARCHAR(32) NULL DEFAULT COMMENT '手机号', +`status` VARCHAR(16) NOT NULL DEFAULT 'active' COMMENT '状态: active/inactive/suspended', +`created_at` DATETIME NULL COMMENT '创建时间', +`updated_at` DATETIME NULL COMMENT '更新时间', +`sync_version` VARCHAR(32) NULL DEFAULT COMMENT '同步版本号', +`cached_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '缓存写入时间', + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +ALTER TABLE `users_cache` ADD INDEX `idx_username` (`username`); + +ALTER TABLE `users_cache` ADD INDEX `idx_orgid` (`orgid`); + +ALTER TABLE `users_cache` ADD INDEX `idx_sync_version` (`sync_version`); diff --git a/json/accounting_records.json b/json/accounting_records.json new file mode 100644 index 0000000..b344d36 --- /dev/null +++ b/json/accounting_records.json @@ -0,0 +1,41 @@ +{ + "accounting_records": { + "params": { + "title": "记账记录管理", + "dbname": "sageapi", + "page_size": 20 + }, + "fields": [ + {"name": "id", "title": "记录ID", "type": "string", "readonly": true, "width": 120}, + {"name": "customer_id", "title": "客户ID", "type": "string", "width": 120}, + {"name": "llmid", "title": "模型ID", "type": "string", "width": 120}, + {"name": "model_name", "title": "模型名称", "type": "string", "width": 140}, + {"name": "pricing_id", "title": "定价ID", "type": "string", "width": 100}, + {"name": "input_tokens", "title": "输入Token", "type": "int", "width": 90, "align": "right"}, + {"name": "output_tokens", "title": "输出Token", "type": "int", "width": 90, "align": "right"}, + {"name": "total_tokens", "title": "总Token", "type": "int", "width": 90, "align": "right"}, + {"name": "quantity", "title": "用量", "type": "float", "width": 80, "align": "right"}, + {"name": "amount", "title": "金额", "type": "float", "width": 90, "align": "right"}, + {"name": "currency", "title": "货币", "type": "string", "width": 60}, + {"name": "request_id", "title": "请求ID", "type": "string", "width": 120}, + {"name": "transno", "title": "事务号", "type": "string", "width": 120}, + {"name": "status", "title": "状态", "type": "select", "width": 80, "options": [{"value": "pending", "label": "待处理"}, {"value": "accounted", "label": "已记账"}, {"value": "failed", "label": "失败"}]}, + {"name": "created_at", "title": "创建时间", "type": "datetime", "width": 160, "readonly": true}, + {"name": "updated_at", "title": "更新时间", "type": "datetime", "width": 160, "readonly": true} + ], + "list": { + "columns": ["id", "customer_id", "model_name", "amount", "total_tokens", "status", "created_at"], + "filters": [ + {"field": "customer_id", "type": "text", "label": "客户ID"}, + {"field": "status", "type": "select", "label": "状态", "options": [{"value": "", "label": "全部"}, {"value": "pending", "label": "待处理"}, {"value": "accounted", "label": "已记账"}, {"value": "failed", "label": "失败"}]}, + {"field": "date_from", "type": "date", "label": "起始日期"}, + {"field": "date_to", "type": "date", "label": "截止日期"} + ], + "sort": {"field": "created_at", "order": "desc"} + }, + "view": {"fields": ["id", "customer_id", "llmid", "model_name", "pricing_id", "input_tokens", "output_tokens", "total_tokens", "quantity", "amount", "currency", "request_id", "transno", "status", "created_at", "updated_at"]}, + "create": {"enabled": false}, + "update": {"enabled": false}, + "delete": {"enabled": false} + } +} diff --git a/json/customer_balance.json b/json/customer_balance.json new file mode 100644 index 0000000..4b4709c --- /dev/null +++ b/json/customer_balance.json @@ -0,0 +1,33 @@ +{ + "customer_balance": { + "params": { + "title": "客户余额管理", + "dbname": "sageapi", + "page_size": 20 + }, + "fields": [ + {"name": "id", "title": "客户ID", "type": "string", "readonly": true, "width": 120}, + {"name": "balance", "title": "余额", "type": "float", "width": 100, "align": "right"}, + {"name": "currency", "title": "货币", "type": "string", "width": 60}, + {"name": "credit_limit", "title": "信用额度", "type": "float", "width": 100, "align": "right"}, + {"name": "last_recharge", "title": "最后充值", "type": "datetime", "width": 160}, + {"name": "last_consumption", "title": "最后消费", "type": "datetime", "width": 160}, + {"name": "status", "title": "状态", "type": "select", "width": 80, "options": [{"value": "active", "label": "正常"}, {"value": "suspended", "label": "暂停"}, {"value": "arrears", "label": "欠费"}]}, + {"name": "cached_at", "title": "缓存时间", "type": "datetime", "width": 160, "readonly": true} + ], + "list": { + "columns": ["id", "balance", "currency", "status", "last_recharge", "cached_at"], + "filters": [ + {"field": "id", "type": "text", "label": "客户ID"}, + {"field": "status", "type": "select", "label": "状态", "options": [{"value": "", "label": "全部"}, {"value": "active", "label": "正常"}, {"value": "suspended", "label": "暂停"}, {"value": "arrears", "label": "欠费"}]} + ] + }, + "view": {"fields": ["id", "balance", "currency", "credit_limit", "last_recharge", "last_consumption", "status", "cached_at"]}, + "create": {"enabled": false}, + "update": { + "enabled": true, + "fields": ["balance", "credit_limit", "status"] + }, + "delete": {"enabled": false} + } +} diff --git a/json/sync_state.json b/json/sync_state.json new file mode 100644 index 0000000..f9e0bf5 --- /dev/null +++ b/json/sync_state.json @@ -0,0 +1,36 @@ +{ + "sync_state": { + "params": { + "title": "同步状态管理", + "dbname": "sageapi", + "page_size": 20 + }, + "fields": [ + {"name": "id", "title": "ID", "type": "string", "readonly": true, "width": 100}, + {"name": "entity_type", "title": "实体类型", "type": "select", "width": 100, "options": [{"value": "users", "label": "用户"}, {"value": "pricing", "label": "定价"}, {"value": "uapi", "label": "UAPI"}, {"value": "llmage", "label": "LLMage"}]}, + {"name": "entity_id", "title": "实体ID", "type": "string", "width": 120}, + {"name": "last_sync_time", "title": "最后同步时间", "type": "datetime", "width": 160}, + {"name": "sync_version", "title": "同步版本", "type": "string", "width": 100}, + {"name": "sync_status", "title": "同步状态", "type": "select", "width": 80, "options": [{"value": "success", "label": "成功"}, {"value": "pending", "label": "待同步"}, {"value": "failed", "label": "失败"}]}, + {"name": "error_msg", "title": "错误信息", "type": "text", "width": 200}, + {"name": "retry_count", "title": "重试次数", "type": "int", "width": 70, "align": "right"}, + {"name": "created_at", "title": "创建时间", "type": "datetime", "width": 160, "readonly": true}, + {"name": "updated_at", "title": "更新时间", "type": "datetime", "width": 160, "readonly": true} + ], + "list": { + "columns": ["entity_type", "entity_id", "sync_status", "last_sync_time", "retry_count", "updated_at"], + "filters": [ + {"field": "entity_type", "type": "select", "label": "实体类型", "options": [{"value": "", "label": "全部"}, {"value": "users", "label": "用户"}, {"value": "pricing", "label": "定价"}, {"value": "uapi", "label": "UAPI"}, {"value": "llmage", "label": "LLMage"}]}, + {"field": "sync_status", "type": "select", "label": "状态", "options": [{"value": "", "label": "全部"}, {"value": "success", "label": "成功"}, {"value": "pending", "label": "待同步"}, {"value": "failed", "label": "失败"}]} + ], + "sort": {"field": "updated_at", "order": "desc"} + }, + "view": {"fields": ["id", "entity_type", "entity_id", "last_sync_time", "sync_version", "sync_status", "error_msg", "retry_count", "created_at", "updated_at"]}, + "create": {"enabled": false}, + "update": { + "enabled": true, + "fields": ["sync_status", "retry_count"] + }, + "delete": {"enabled": false} + } +} diff --git a/scripts/generate_ddl.py b/scripts/generate_ddl.py new file mode 100755 index 0000000..62c5b26 --- /dev/null +++ b/scripts/generate_ddl.py @@ -0,0 +1,55 @@ +#!/usr/bin/env python3 +"""Generate DDL from SageAPI model JSON definitions.""" +import json +import os + +def generate_ddl(models_dir='models', output_path='db/schema.sql'): + os.makedirs(os.path.dirname(output_path), exist_ok=True) + ddl_lines = ['-- SageAPI DDL (auto-generated)', ''] + + for f in sorted(os.listdir(models_dir)): + if not f.endswith('.json'): + continue + with open(os.path.join(models_dir, f)) as fh: + data = json.load(fh) + + summary = data['summary'][0] + tblname = summary['name'] + primary = summary['primary'] + btick = '`' + + ddl_lines.append(f'CREATE TABLE IF NOT EXISTS {btick}{tblname}{btick} (') + + col_defs = [] + for field in data['fields']: + nullable = 'NULL' if field.get('nullable', True) else 'NOT NULL' + default = '' + if field.get('default') is not None: + if isinstance(field['default'], str) and field['default'] not in ('CURRENT_TIMESTAMP', 'NULL', ''): + default = f"DEFAULT '{field['default']}'" + else: + default = f"DEFAULT {field['default']}" + comment = f"COMMENT '{field.get('comment', '')}'" + fname = f'{btick}{field["name"]}{btick}' + col_defs.append(f' {fname} {field["type"]} {nullable} {default} {comment}'.strip()) + + pk = f'{btick}{primary}{btick}' + col_defs.append(f' PRIMARY KEY ({pk})') + ddl_lines.append(',\n'.join(col_defs)) + ddl_lines.append(') ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;') + ddl_lines.append('') + + tbl = f'{btick}{tblname}{btick}' + for idx in data.get('idxfields', []): + unique = 'UNIQUE' if idx.get('unique') else 'INDEX' + cols = ','.join(f'{btick}{c}{btick}' for c in idx['fields']) + idx_name = f'{btick}{idx["name"]}{btick}' + ddl_lines.append(f'ALTER TABLE {tbl} ADD {unique} {idx_name} ({cols});') + ddl_lines.append('') + + with open(output_path, 'w') as fh: + fh.write('\n'.join(ddl_lines)) + print(f' Generated: {output_path} ({len(ddl_lines)} lines)') + +if __name__ == '__main__': + generate_ddl() diff --git a/scripts/validate_crud.py b/scripts/validate_crud.py new file mode 100755 index 0000000..64a0ecd --- /dev/null +++ b/scripts/validate_crud.py @@ -0,0 +1,32 @@ +#!/usr/bin/env python3 +"""Validate SageAPI CRUD JSON definitions.""" +import json +import os +import sys + +def validate_crud(json_dir='json'): + errors = 0 + for f in sorted(os.listdir(json_dir)): + if not f.endswith('.json'): + continue + filepath = os.path.join(json_dir, f) + with open(filepath) as fh: + data = json.load(fh) + + for tblname, config in data.items(): + if 'params' not in config: + print(f'ERROR: {f} root key "{tblname}" missing "params"') + errors += 1 + if 'fields' not in config: + print(f'ERROR: {f} root key "{tblname}" missing "fields"') + errors += 1 + if not errors: + print(f' OK: {f} ({tblname})') + + if errors: + print(f'\n{errors} error(s) found') + sys.exit(1) + print('All CRUD definitions valid') + +if __name__ == '__main__': + validate_crud() diff --git a/scripts/validate_models.py b/scripts/validate_models.py new file mode 100755 index 0000000..27e3d5e --- /dev/null +++ b/scripts/validate_models.py @@ -0,0 +1,41 @@ +#!/usr/bin/env python3 +"""Validate SageAPI model JSON definitions.""" +import json +import os +import sys + +def validate_models(models_dir='models'): + errors = 0 + for f in sorted(os.listdir(models_dir)): + if not f.endswith('.json'): + continue + filepath = os.path.join(models_dir, f) + with open(filepath) as fh: + data = json.load(fh) + + for key in ['summary', 'fields', 'idxfields']: + if key not in data: + print(f'ERROR: {f} missing "{key}"') + errors += 1 + + if not data.get('summary') or not isinstance(data['summary'], list): + print(f'ERROR: {f} summary must be a non-empty list') + errors += 1 + continue + + summary = data['summary'][0] + if 'primary' not in summary: + print(f'ERROR: {f} summary must have "primary" field') + errors += 1 + + table_name = summary.get('name', '?') + primary = summary.get('primary', '?') + print(f' OK: {f} ({table_name}, primary={primary})') + + if errors: + print(f'\n{errors} error(s) found') + sys.exit(1) + print('All model definitions valid') + +if __name__ == '__main__': + validate_models()