From c474993b0a67496daf4852f92cb5b4fcfcfee898 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 14 Jan 2026 03:00:41 +0000 Subject: [PATCH 1/4] Initial plan From acac17b26a181aa5c36b478f5978be253798d5e4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 14 Jan 2026 03:13:19 +0000 Subject: [PATCH 2/4] Organize docs folder with comprehensive metadata and ObjectQL documentation Co-authored-by: baozhoutao <6194462+baozhoutao@users.noreply.github.com> --- docs/README.md | 70 ++- docs/metadata/README.md | 147 +++++ docs/metadata/field-types.md | 726 +++++++++++++++++++++++ docs/metadata/inheritance-rules.md | 508 ++++++++++++++++ docs/metadata/metadata-types.md | 369 ++++++++++++ docs/metadata/object-metadata.md | 685 +++++++++++++++++++++ docs/metadata/permissions.md | 578 ++++++++++++++++++ docs/{objectql.md => objectql/README.md} | 0 docs/objectql/best-practices.md | 603 +++++++++++++++++++ docs/objectql/filter-operators.md | 625 +++++++++++++++++++ docs/objectql/query-syntax.md | 524 ++++++++++++++++ docs/{trigger.md => triggers/README.md} | 0 docs/triggers/trigger-context.md | 644 ++++++++++++++++++++ docs/triggers/trigger-types.md | 604 +++++++++++++++++++ 14 files changed, 6065 insertions(+), 18 deletions(-) create mode 100644 docs/metadata/README.md create mode 100644 docs/metadata/field-types.md create mode 100644 docs/metadata/inheritance-rules.md create mode 100644 docs/metadata/metadata-types.md create mode 100644 docs/metadata/object-metadata.md create mode 100644 docs/metadata/permissions.md rename docs/{objectql.md => objectql/README.md} (100%) create mode 100644 docs/objectql/best-practices.md create mode 100644 docs/objectql/filter-operators.md create mode 100644 docs/objectql/query-syntax.md rename docs/{trigger.md => triggers/README.md} (100%) create mode 100644 docs/triggers/trigger-context.md create mode 100644 docs/triggers/trigger-types.md diff --git a/docs/README.md b/docs/README.md index 5e5bbce9ec..a1945ea62c 100644 --- a/docs/README.md +++ b/docs/README.md @@ -13,9 +13,23 @@ Welcome to Steedos Platform documentation! - **[Quick Reference](./QUICK_REFERENCE.md)** - Quick reference for commands, APIs, and configurations ### Technical Documentation -- **[ObjectQL](./objectql.md)** - Object Query Language detailed documentation +- **[Metadata](./metadata/)** - Complete metadata documentation including inheritance rules + - [Metadata Overview](./metadata/README.md) + - [Metadata Types](./metadata/metadata-types.md) + - [Inheritance Rules](./metadata/inheritance-rules.md) + - [Object Metadata](./metadata/object-metadata.md) + - [Field Types](./metadata/field-types.md) + - [Permissions](./metadata/permissions.md) +- **[ObjectQL](./objectql/)** - Object Query Language complete guide + - [ObjectQL Overview](./objectql/README.md) + - [Query Syntax](./objectql/query-syntax.md) + - [Filter Operators](./objectql/filter-operators.md) + - [Best Practices](./objectql/best-practices.md) +- **[Triggers](./triggers/)** - Trigger development guide + - [Trigger Overview](./triggers/README.md) + - [Trigger Types](./triggers/trigger-types.md) + - [Trigger Context](./triggers/trigger-context.md) - **[Object Service](./object-service.md)** - Object service architecture -- **[Trigger](./trigger.md)** - Trigger usage guide - **[Environment Variables](./env.md)** - Environment variable configuration ## 🚀 Quick Start @@ -49,17 +63,22 @@ Visit http://localhost:5100 ### For Beginners 1. Read [README](../README.md) to understand project overview 2. Follow Quick Start to deploy your first application -3. Learn [ObjectQL](./objectql.md) for data operations +3. Learn [Metadata Basics](./metadata/) to understand data models +4. Learn [ObjectQL](./objectql/) for data operations ### For Developers 1. Read [Core Architecture](./CORE_ARCHITECTURE_EN.md) 2. Follow [Developer Guide](./DEVELOPER_GUIDE.md) to set up development environment -3. Browse [Packages Index](./PACKAGES_INDEX.md) to understand each module +3. Understand [Metadata System](./metadata/) and [Inheritance Rules](./metadata/inheritance-rules.md) +4. Learn [Trigger Development](./triggers/) for business logic +5. Master [ObjectQL Best Practices](./objectql/best-practices.md) +6. Browse [Packages Index](./PACKAGES_INDEX.md) to understand each module ### For Advanced Users -1. Deep dive into microservices architecture and extension mechanisms -2. Customize triggers and business logic -3. Integrate third-party systems and AI services +1. Deep dive into [Metadata Inheritance](./metadata/inheritance-rules.md) and override mechanisms +2. Customize [Triggers](./triggers/) and implement complex business logic +3. Optimize [ObjectQL Queries](./objectql/query-syntax.md) for performance +4. Integrate third-party systems and AI services ## 🔗 Resources @@ -73,17 +92,32 @@ Visit http://localhost:5100 ``` docs/ -├── README.md # This document -├── CORE_ARCHITECTURE_EN.md # Core architecture -├── DEVELOPER_GUIDE.md # Developer guide -├── PACKAGES_INDEX.md # Packages and services index -├── QUICK_REFERENCE.md # Quick reference -├── objectql.md # ObjectQL documentation -├── object-service.md # Object service documentation -├── trigger.md # Trigger documentation -├── env.md # Environment variables -├── cn/ # Chinese documentation directory -└── images/ # Image resources +├── README.md # This document +├── CORE_ARCHITECTURE_EN.md # Core architecture +├── DEVELOPER_GUIDE.md # Developer guide +├── PACKAGES_INDEX.md # Packages and services index +├── QUICK_REFERENCE.md # Quick reference +├── object-service.md # Object service documentation +├── env.md # Environment variables +├── metadata/ # Metadata documentation (NEW) +│ ├── README.md # Metadata overview +│ ├── metadata-types.md # Metadata types reference +│ ├── inheritance-rules.md # Inheritance and override rules +│ ├── object-metadata.md # Object metadata detailed guide +│ ├── field-types.md # Field types complete reference +│ └── permissions.md # Permissions configuration +├── objectql/ # ObjectQL documentation (NEW) +│ ├── README.md # ObjectQL overview +│ ├── query-syntax.md # Query syntax detailed guide +│ ├── filter-operators.md # Filter operators reference +│ └── best-practices.md # ObjectQL best practices +├── triggers/ # Trigger documentation (NEW) +│ ├── README.md # Trigger overview +│ ├── trigger-types.md # Trigger types and use cases +│ └── trigger-context.md # Trigger context reference +├── cn/ # Chinese documentation directory +├── diagrams/ # Architecture diagrams +└── images/ # Image resources ``` ## 🤝 Contributing diff --git a/docs/metadata/README.md b/docs/metadata/README.md new file mode 100644 index 0000000000..cd9a7791a4 --- /dev/null +++ b/docs/metadata/README.md @@ -0,0 +1,147 @@ +# Steedos Platform 元数据文档 + +## 概述 (Overview) + +元数据 (Metadata) 是 Steedos 平台的核心概念,用于定义业务对象、字段、权限、界面布局等。通过元数据配置,您可以快速构建企业应用,无需编写大量代码。 + +## 什么是元数据? + +在 Steedos 平台中,元数据是描述应用程序结构和行为的配置文件。这些配置文件以 YAML 或 JavaScript 格式存储,包括: + +- **对象定义** (Objects) - 定义业务实体及其字段 +- **字段定义** (Fields) - 定义对象的属性和数据类型 +- **应用定义** (Applications) - 定义应用及其包含的对象 +- **页面布局** (Layouts) - 定义界面的显示方式 +- **权限集** (Permission Sets) - 定义用户权限 +- **标签页** (Tabs) - 定义导航标签 +- **触发器** (Triggers) - 定义业务逻辑 + +## 元数据类型 + +### 核心元数据类型 + +| 类型 | 文件扩展名 | 说明 | 文档链接 | +|------|-----------|------|---------| +| Object | `.object.yml` | 对象定义 | [详细说明](./object-metadata.md) | +| Field | 在 object 文件中定义 | 字段定义 | [字段类型](./field-types.md) | +| Application | `.app.yml` | 应用定义 | - | +| Layout | `.layout.yml` | 页面布局 | - | +| Permission Set | `.permissionset.yml` | 权限集 | [权限说明](./permissions.md) | +| Tab | `.tab.yml` | 标签页 | - | +| Trigger | `.trigger.js` | 触发器 | [触发器文档](../triggers/) | + +## 元数据继承和覆盖 + +Steedos 平台支持元数据的继承和覆盖机制,允许在不同的软件包中扩展和定制元数据。详细说明请参阅: + +- [元数据继承规则](./inheritance-rules.md) + +## 元数据文件位置 + +元数据文件通常位于软件包的特定目录结构中: + +``` +your-package/ +└── main/ + └── default/ + ├── objects/ # 对象定义 + │ └── accounts/ + │ └── accounts.object.yml + ├── applications/ # 应用定义 + │ └── app.app.yml + ├── layouts/ # 页面布局 + │ └── layout.layout.yml + ├── permissionsets/ # 权限集 + │ └── admin.permissionset.yml + ├── tabs/ # 标签页 + │ └── tab.tab.yml + └── triggers/ # 触发器 + └── trigger.trigger.js +``` + +## 元数据加载过程 + +1. **扫描阶段** - 系统启动时扫描所有软件包中的元数据文件 +2. **解析阶段** - 解析 YAML/JS 文件,验证语法 +3. **合并阶段** - 根据依赖关系和继承规则合并元数据 +4. **生效阶段** - 生成运行时对象和服务 + +## 元数据开发工具 + +### VSCode 扩展 + +使用 `Steedos Extension Pack` VSCode 扩展可以: +- 语法高亮和自动补全 +- 元数据同步 (retrieve/deploy) +- 实时验证 + +### CLI 工具 + +```bash +# 导出元数据 +steedos source:retrieve + +# 部署元数据 +steedos source:deploy + +# 验证元数据 +steedos source:validate +``` + +## 快速开始 + +### 创建一个简单的对象 + +```yaml +# objects/custom_product.object.yml +name: custom_product +label: 产品 +icon: product +fields: + name: + type: text + label: 产品名称 + required: true + price: + type: currency + label: 价格 + description: + type: textarea + label: 描述 +``` + +### 创建一个应用 + +```yaml +# applications/sales.app.yml +name: sales +label: 销售管理 +icon: apps +objects: + - custom_product + - accounts + - contacts +``` + +## 相关文档 + +- [对象元数据详细说明](./object-metadata.md) +- [字段类型完整参考](./field-types.md) +- [元数据继承规则](./inheritance-rules.md) +- [权限配置](./permissions.md) +- [ObjectQL 查询语言](../objectql/) +- [触发器开发](../triggers/) + +## 最佳实践 + +1. **使用描述性名称** - 对象和字段名称应该清晰表达其用途 +2. **合理组织目录** - 按功能模块组织元数据文件 +3. **版本控制** - 使用 Git 管理元数据变更 +4. **文档注释** - 在元数据文件中添加必要的注释 +5. **遵循命名规范** - 使用统一的命名约定 + +## 参考资源 + +- [开发者指南](../DEVELOPER_GUIDE.md) +- [核心架构](../CORE_ARCHITECTURE_EN.md) +- [包和服务索引](../PACKAGES_INDEX.md) diff --git a/docs/metadata/field-types.md b/docs/metadata/field-types.md new file mode 100644 index 0000000000..b03cb6900a --- /dev/null +++ b/docs/metadata/field-types.md @@ -0,0 +1,726 @@ +# 字段类型完整参考 + +## 概述 + +Steedos 平台支持丰富的字段类型,用于定义对象的属性和数据结构。每种字段类型都有特定的用途和配置选项。 + +## 基础字段类型 + +### text (文本) + +单行文本字段,用于存储短文本。 + +**属性**: +```yaml +field_name: + type: text + label: 字段标签 + required: false # 是否必填 + searchable: true # 是否可搜索 + defaultValue: "" # 默认值 + maxlength: 255 # 最大长度 + is_wide: false # 是否宽字段 + readonly: false # 是否只读 + hidden: false # 是否隐藏 +``` + +**示例**: +```yaml +name: + type: text + label: 名称 + required: true + searchable: true + maxlength: 100 +``` + +### textarea (多行文本) + +多行文本字段,用于存储较长的文本内容。 + +**属性**: +```yaml +field_name: + type: textarea + label: 字段标签 + required: false + rows: 3 # 显示行数 + is_wide: true # 默认宽字段 +``` + +**示例**: +```yaml +description: + type: textarea + label: 描述 + rows: 5 + is_wide: true +``` + +### html (富文本) + +HTML 富文本编辑器字段。 + +**属性**: +```yaml +field_name: + type: html + label: 字段标签 + required: false + is_wide: true +``` + +**示例**: +```yaml +content: + type: html + label: 内容 + is_wide: true +``` + +## 数值字段类型 + +### number (数字) + +数值字段,支持整数和小数。 + +**属性**: +```yaml +field_name: + type: number + label: 字段标签 + required: false + scale: 2 # 小数位数 + precision: 18 # 总位数 + min: 0 # 最小值 + max: 999999 # 最大值 + defaultValue: 0 +``` + +**示例**: +```yaml +quantity: + type: number + label: 数量 + scale: 0 + min: 0 + defaultValue: 1 + +score: + type: number + label: 评分 + scale: 2 + min: 0 + max: 100 +``` + +### currency (货币) + +货币字段,带有货币符号。 + +**属性**: +```yaml +field_name: + type: currency + label: 字段标签 + required: false + scale: 2 # 小数位数,默认 2 + precision: 18 # 总位数 + defaultValue: 0 +``` + +**示例**: +```yaml +amount: + type: currency + label: 金额 + scale: 2 + defaultValue: 0 + +total: + type: currency + label: 总计 + scale: 2 + summary_type: sum # 汇总类型 +``` + +### percent (百分比) + +百分比字段。 + +**属性**: +```yaml +field_name: + type: percent + label: 字段标签 + required: false + scale: 2 + min: 0 + max: 100 +``` + +**示例**: +```yaml +discount: + type: percent + label: 折扣 + scale: 2 + min: 0 + max: 100 +``` + +## 日期时间字段类型 + +### date (日期) + +日期字段,只包含日期不包含时间。 + +**属性**: +```yaml +field_name: + type: date + label: 字段标签 + required: false + defaultValue: null # 可以是日期字符串或特殊值如 "now" +``` + +**示例**: +```yaml +birth_date: + type: date + label: 出生日期 + +contract_date: + type: date + label: 合同日期 + required: true +``` + +### datetime (日期时间) + +日期时间字段,包含日期和时间。 + +**属性**: +```yaml +field_name: + type: datetime + label: 字段标签 + required: false + defaultValue: null +``` + +**示例**: +```yaml +meeting_time: + type: datetime + label: 会议时间 + required: true + +last_contact: + type: datetime + label: 最后联系时间 +``` + +### time (时间) + +时间字段,只包含时间不包含日期。 + +**属性**: +```yaml +field_name: + type: time + label: 字段标签 + required: false +``` + +**示例**: +```yaml +start_time: + type: time + label: 开始时间 +``` + +## 选择字段类型 + +### boolean (布尔) + +布尔字段,true/false 值。 + +**属性**: +```yaml +field_name: + type: boolean + label: 字段标签 + required: false + defaultValue: false +``` + +**示例**: +```yaml +is_active: + type: boolean + label: 是否启用 + defaultValue: true + +is_vip: + type: boolean + label: VIP客户 + defaultValue: false +``` + +### select (下拉选择) + +单选下拉字段。 + +**属性**: +```yaml +field_name: + type: select + label: 字段标签 + required: false + options: # 选项列表 + - label: 显示文本 + value: 值 + defaultValue: null + multiple: false # 是否多选 +``` + +**示例**: +```yaml +status: + type: select + label: 状态 + options: + - label: 草稿 + value: draft + - label: 进行中 + value: in_progress + - label: 已完成 + value: completed + - label: 已取消 + value: cancelled + defaultValue: draft + required: true + +tags: + type: select + label: 标签 + multiple: true # 多选 + options: + - label: 重要 + value: important + - label: 紧急 + value: urgent + - label: 待处理 + value: pending +``` + +### picklist (选择列表) + +与 select 类似,但使用不同的 UI 控件。 + +**示例**: +```yaml +category: + type: picklist + label: 类别 + options: + - label: 类别A + value: a + - label: 类别B + value: b +``` + +## 关系字段类型 + +### lookup (查找关系) + +查找关系字段,关联到另一个对象。 + +**属性**: +```yaml +field_name: + type: lookup + label: 字段标签 + required: false + reference_to: target_object # 目标对象名称 + reference_to_field: _id # 目标字段,默认 _id + multiple: false # 是否多选 + filters: [] # 过滤条件 + depend_on: [] # 依赖字段 +``` + +**示例**: +```yaml +account: + type: lookup + label: 客户 + reference_to: accounts + required: true + +owner: + type: lookup + label: 负责人 + reference_to: users + defaultValue: "{userId}" + +contacts: + type: lookup + label: 联系人 + reference_to: contacts + multiple: true + filters: [['account', '=', '{account}']] +``` + +### master_detail (主从关系) + +主从关系字段,比 lookup 更紧密的关联关系。 + +**特点**: +- 从记录必须有主记录 +- 删除主记录时级联删除从记录 +- 从记录继承主记录的共享设置 + +**属性**: +```yaml +field_name: + type: master_detail + label: 字段标签 + required: true # 必填 + reference_to: target_object +``` + +**示例**: +```yaml +order: + type: master_detail + label: 订单 + reference_to: sales_orders + required: true +``` + +## 特殊字段类型 + +### autonumber (自动编号) + +自动编号字段,按规则自动生成唯一编号。 + +**属性**: +```yaml +field_name: + type: autonumber + label: 字段标签 + formula: "PRE-{0000}" # 编号格式 + readonly: true +``` + +**示例**: +```yaml +order_number: + type: autonumber + label: 订单号 + formula: "SO-{YYYY}{MM}{DD}-{0000}" + +contract_no: + type: autonumber + label: 合同编号 + formula: "CT-{0000}" +``` + +### formula (公式) + +公式字段,根据其他字段自动计算。 + +**属性**: +```yaml +field_name: + type: formula + label: 字段标签 + formula: "field1 + field2" # 公式表达式 + data_type: number # 返回值类型 + scale: 2 # 数值精度 +``` + +**示例**: +```yaml +total_amount: + type: formula + label: 总金额 + formula: "quantity * unit_price" + data_type: currency + scale: 2 + +full_name: + type: formula + label: 全名 + formula: "first_name + ' ' + last_name" + data_type: text + +age: + type: formula + label: 年龄 + formula: "YEAR(TODAY()) - YEAR(birth_date)" + data_type: number +``` + +### summary (汇总) + +汇总字段,汇总相关对象的数据。 + +**属性**: +```yaml +field_name: + type: summary + label: 字段标签 + summary_object: related_object # 相关对象 + summary_type: count # 汇总类型 + summary_field: field_name # 汇总字段 + filters: [] # 过滤条件 +``` + +**汇总类型**: +- `count` - 计数 +- `sum` - 求和 +- `min` - 最小值 +- `max` - 最大值 +- `avg` - 平均值 + +**示例**: +```yaml +order_count: + type: summary + label: 订单数量 + summary_object: sales_orders + summary_type: count + +total_revenue: + type: summary + label: 总收入 + summary_object: sales_orders + summary_type: sum + summary_field: amount + filters: [['status', '=', 'completed']] +``` + +### grid (子表) + +子表字段,在主表中嵌入子表。 + +**属性**: +```yaml +field_name: + type: grid + label: 字段标签 + is_wide: true +``` + +**示例**: +```yaml +order_items: + type: grid + label: 订单明细 + is_wide: true +``` + +## 文件字段类型 + +### file (文件) + +文件上传字段。 + +**属性**: +```yaml +field_name: + type: file + label: 字段标签 + multiple: false # 是否支持多文件 +``` + +**示例**: +```yaml +attachment: + type: file + label: 附件 + multiple: true + +contract: + type: file + label: 合同文件 + multiple: false +``` + +### image (图片) + +图片上传字段。 + +**属性**: +```yaml +field_name: + type: image + label: 字段标签 + multiple: false +``` + +**示例**: +```yaml +avatar: + type: image + label: 头像 + multiple: false + +photos: + type: image + label: 照片 + multiple: true +``` + +## 地址和位置字段 + +### url (URL) + +URL 地址字段。 + +**属性**: +```yaml +field_name: + type: url + label: 字段标签 + required: false +``` + +**示例**: +```yaml +website: + type: url + label: 网站 +``` + +### email (邮箱) + +邮箱地址字段,自动验证格式。 + +**属性**: +```yaml +field_name: + type: email + label: 字段标签 + required: false + multiple: false # 是否支持多个邮箱 +``` + +**示例**: +```yaml +email: + type: email + label: 邮箱地址 + required: true + +cc_emails: + type: email + label: 抄送邮箱 + multiple: true +``` + +### location (地理位置) + +地理位置字段,存储经纬度。 + +**属性**: +```yaml +field_name: + type: location + label: 字段标签 +``` + +**示例**: +```yaml +office_location: + type: location + label: 办公地点 +``` + +## 系统字段 + +以下字段由系统自动维护,不需要在元数据中定义: + +| 字段名 | 类型 | 说明 | +|--------|------|------| +| _id | String | 记录唯一标识符 | +| name | Text | 记录名称(主字段) | +| owner | Lookup | 记录所有者 | +| space | String | 工作区 | +| created | Datetime | 创建时间 | +| created_by | Lookup | 创建人 | +| modified | Datetime | 修改时间 | +| modified_by | Lookup | 修改人 | +| locked | Boolean | 是否锁定 | +| company_id | String | 分部 ID | +| company_ids | Array | 所属分部列表 | + +## 字段通用属性 + +所有字段类型都支持以下通用属性: + +```yaml +field_name: + type: field_type + label: 字段标签 # 显示名称 + name: field_name # API 名称 + required: false # 是否必填 + readonly: false # 是否只读 + hidden: false # 是否隐藏 + omit: false # 是否排除 + group: "分组名" # 字段分组 + sortable: true # 是否可排序 + searchable: true # 是否可搜索 + filterable: true # 是否可过滤 + inlineHelpText: "" # 帮助文本 + description: "" # 字段描述 + defaultValue: null # 默认值 +``` + +## 字段命名规范 + +1. **使用小写字母和下划线** + - 正确: `customer_name`, `order_date` + - 错误: `CustomerName`, `orderDate` + +2. **避免使用保留字** + - 不要使用: `id`, `name`, `owner`, `created` 等系统字段名 + +3. **使用描述性名称** + - 正确: `annual_revenue`, `contact_phone` + - 错误: `ar`, `cp` + +4. **保持一致性** + - 统一使用 `_date` 或 `_time` 后缀表示日期时间字段 + - 统一使用 `is_` 前缀表示布尔字段 + +## 字段最佳实践 + +1. **合理设置必填字段** + - 只将真正必须的字段设为 required + - 考虑用户体验,避免过多必填字段 + +2. **使用合适的字段类型** + - 根据数据特点选择最合适的类型 + - 邮箱使用 email 类型而不是 text + +3. **设置合理的默认值** + - 为常用字段设置合理的默认值 + - 减少用户输入工作量 + +4. **使用公式字段减少冗余** + - 可以计算的字段使用 formula + - 保持数据一致性 + +5. **合理使用关系字段** + - lookup 用于松散关联 + - master_detail 用于紧密关联 + +## 相关文档 + +- [对象元数据](./object-metadata.md) +- [元数据类型](./metadata-types.md) +- [元数据继承规则](./inheritance-rules.md) +- [ObjectQL 查询](../objectql/) diff --git a/docs/metadata/inheritance-rules.md b/docs/metadata/inheritance-rules.md new file mode 100644 index 0000000000..e7b5073858 --- /dev/null +++ b/docs/metadata/inheritance-rules.md @@ -0,0 +1,508 @@ +# 元数据继承和覆盖规则 + +## 概述 + +Steedos 平台支持强大的元数据继承和覆盖机制,允许在不同的软件包中扩展和定制已有的元数据。这种机制使得您可以: + +- 在新包中扩展标准对象 +- 覆盖现有字段的属性 +- 添加新字段到现有对象 +- 定制权限和布局 + +## 软件包依赖和加载顺序 + +### 软件包结构 + +Steedos 平台中的软件包按依赖关系组织: + +``` +standard-objects (基础包) + ↓ +custom-package (自定义包) + ↓ +customer-package (客户定制包) +``` + +### 加载顺序 + +系统启动时,元数据按以下顺序加载: + +1. **扫描软件包** - 扫描所有软件包目录 +2. **解析依赖关系** - 分析包之间的依赖关系 +3. **按序加载** - 按依赖顺序加载元数据 +4. **合并元数据** - 根据继承规则合并同名元数据 + +## 对象继承规则 + +### 基本继承原则 + +当多个软件包中定义了相同名称的对象时,系统会自动合并这些定义: + +**基础包** (`standard-objects/accounts.object.yml`): +```yaml +name: accounts +label: 业务伙伴 +fields: + name: + type: text + label: 名称 + required: true + phone: + type: text + label: 电话 +``` + +**扩展包** (`custom-package/accounts.object.yml`): +```yaml +name: accounts +fields: + industry: + type: select + label: 行业 + options: + - label: IT + value: it + - label: 制造业 + value: manufacturing + email: + type: email + label: 邮箱 +``` + +**合并结果**: +```yaml +name: accounts +label: 业务伙伴 # 保留基础包的 label +fields: + name: + type: text + label: 名称 + required: true + phone: + type: text + label: 电话 + industry: # 新增字段 + type: select + label: 行业 + options: + - label: IT + value: it + - label: 制造业 + value: manufacturing + email: # 新增字段 + type: email + label: 邮箱 +``` + +### 对象属性覆盖 + +后加载的包可以覆盖先加载包的对象属性: + +**基础包**: +```yaml +name: custom_object +label: 自定义对象 +enable_search: false +enable_files: false +``` + +**扩展包**: +```yaml +name: custom_object +label: 定制对象 # 覆盖 label +enable_search: true # 覆盖 enable_search +enable_files: true # 覆盖 enable_files +``` + +**合并结果**: +```yaml +name: custom_object +label: 定制对象 # 使用扩展包的值 +enable_search: true # 使用扩展包的值 +enable_files: true # 使用扩展包的值 +``` + +## 字段继承规则 + +### 添加新字段 + +在扩展包中直接添加新字段: + +```yaml +name: accounts +fields: + custom_field: + type: text + label: 自定义字段 +``` + +### 覆盖字段属性 + +覆盖已有字段的特定属性: + +**基础包**: +```yaml +name: accounts +fields: + phone: + type: text + label: 电话 + required: false +``` + +**扩展包**: +```yaml +name: accounts +fields: + phone: + required: true # 只覆盖 required 属性 + label: 联系电话 # 覆盖 label 属性 +``` + +**合并结果**: +```yaml +name: accounts +fields: + phone: + type: text # 保留原值 + label: 联系电话 # 使用新值 + required: true # 使用新值 +``` + +### 字段属性合并规则 + +| 属性类型 | 合并规则 | 说明 | +|---------|---------|------| +| 简单类型 (String, Number, Boolean) | 完全覆盖 | 后加载的值覆盖先加载的值 | +| 数组类型 (Array) | 追加合并 | 后加载的值追加到数组末尾 | +| 对象类型 (Object) | 深度合并 | 递归合并对象属性 | + +**数组合并示例**: + +**基础包**: +```yaml +name: accounts +fields: + status: + type: select + options: + - label: 活跃 + value: active + - label: 停用 + value: inactive +``` + +**扩展包**: +```yaml +name: accounts +fields: + status: + options: + - label: 待审核 + value: pending +``` + +**合并结果**: +```yaml +name: accounts +fields: + status: + type: select + options: + - label: 活跃 + value: active + - label: 停用 + value: inactive + - label: 待审核 # 追加新选项 + value: pending +``` + +## 列表视图继承规则 + +### 添加新视图 + +```yaml +name: accounts +list_views: + custom_view: + label: 自定义视图 + columns: [name, phone, email] + filters: [['status', '=', 'active']] +``` + +### 覆盖已有视图 + +**基础包**: +```yaml +name: accounts +list_views: + all: + label: 所有 + columns: [name, phone] +``` + +**扩展包**: +```yaml +name: accounts +list_views: + all: + label: 所有记录 # 覆盖 label + columns: [name, phone, email] # 覆盖 columns +``` + +## 权限继承规则 + +### 权限合并 + +权限设置采用**最小权限原则**进行合并: + +**基础包**: +```yaml +name: sales_user +object_permissions: + accounts: + allowCreate: true + allowRead: true + allowEdit: true + allowDelete: true +``` + +**扩展包**: +```yaml +name: sales_user +object_permissions: + accounts: + allowDelete: false # 收回删除权限 +``` + +**合并结果**: +```yaml +name: sales_user +object_permissions: + accounts: + allowCreate: true + allowRead: true + allowEdit: true + allowDelete: false # 使用更严格的权限 +``` + +## 触发器继承规则 + +### 触发器不合并 + +触发器采用**完全覆盖**策略,后加载的触发器会完全替换先加载的同名触发器。 + +**基础包** (`accounts.trigger.js`): +```javascript +module.exports = { + listenTo: 'accounts', + beforeInsert: async function() { + // 基础逻辑 + } +}; +``` + +**扩展包** (`accounts.trigger.js`): +```javascript +module.exports = { + listenTo: 'accounts', + beforeInsert: async function() { + // 这会完全替换基础包的 beforeInsert + }, + afterInsert: async function() { + // 新增的触发器 + } +}; +``` + +### 多个触发器文件 + +可以使用不同的文件名定义多个触发器: + +``` +triggers/ + ├── accounts_validation.trigger.js + ├── accounts_notification.trigger.js + └── accounts_workflow.trigger.js +``` + +## 应用继承规则 + +### 对象列表合并 + +应用中的对象列表会自动合并: + +**基础包**: +```yaml +_id: sales +objects: + - accounts + - contacts +``` + +**扩展包**: +```yaml +_id: sales +objects: + - sales_order + - products +``` + +**合并结果**: +```yaml +_id: sales +objects: + - accounts + - contacts + - sales_order # 追加 + - products # 追加 +``` + +## 布局继承规则 + +### 区块合并 + +布局的区块(sections)会按名称合并: + +**基础包**: +```yaml +name: accounts_default +sections: + - label: 基本信息 + fields: [name, phone] +``` + +**扩展包**: +```yaml +name: accounts_default +sections: + - label: 基本信息 + fields: [email] # 追加到同名区块 + - label: 扩展信息 + fields: [industry] # 新增区块 +``` + +## 继承最佳实践 + +### 1. 明确包的依赖关系 + +在 `package.json` 中声明依赖: + +```json +{ + "name": "custom-package", + "dependencies": { + "@steedos/standard-objects": "^2.0.0" + } +} +``` + +### 2. 使用描述性包名 + +``` +standard-objects/ # 标准对象包 +crm-extension/ # CRM 扩展包 +customer-custom/ # 客户定制包 +``` + +### 3. 最小化覆盖范围 + +只覆盖需要修改的属性,保留其他默认值: + +```yaml +# 推荐:只覆盖需要的属性 +name: accounts +fields: + phone: + required: true + +# 不推荐:重复定义所有属性 +name: accounts +fields: + phone: + type: text + label: 电话 + required: true +``` + +### 4. 文档化定制内容 + +在扩展包中添加注释说明定制原因: + +```yaml +name: accounts +# 为满足客户需求,添加行业字段 +fields: + industry: + type: select + label: 行业 +``` + +### 5. 避免循环依赖 + +确保包之间没有循环依赖关系: + +``` +# 错误:循环依赖 +package-a → package-b +package-b → package-a + +# 正确:单向依赖 +package-a → package-b → standard-objects +``` + +## 继承调试 + +### 查看合并后的元数据 + +使用 API 查看最终合并的元数据: + +```javascript +// 获取对象定义 +const objectMeta = await broker.call('metadata.getObject', { + objectName: 'accounts' +}); +console.log(objectMeta); +``` + +### 元数据重载 + +修改元数据后重新加载: + +```bash +# 调用重载 API +curl -X POST http://localhost:5000/api/metadata/reload +``` + +### 调试日志 + +启用调试日志查看加载过程: + +```bash +# 设置环境变量 +export DEBUG=metadata:* +``` + +## 常见问题 + +### Q: 为什么我的字段没有显示? + +A: 检查以下几点: +1. 字段是否在正确的对象下定义 +2. 包的加载顺序是否正确 +3. 是否需要重新加载元数据 + +### Q: 如何完全替换而不是合并? + +A: 触发器使用完全替换策略。对于其他元数据,需要在扩展包中重新定义所有属性。 + +### Q: 多个包修改同一属性时如何确定优先级? + +A: 按包的加载顺序,后加载的包优先级更高。 + +## 相关文档 + +- [元数据概述](./README.md) +- [元数据类型](./metadata-types.md) +- [对象元数据](./object-metadata.md) +- [对象服务](../object-service.md) diff --git a/docs/metadata/metadata-types.md b/docs/metadata/metadata-types.md new file mode 100644 index 0000000000..91cffc41bf --- /dev/null +++ b/docs/metadata/metadata-types.md @@ -0,0 +1,369 @@ +# 元数据类型详细说明 + +## Objects (对象) + +对象是 Steedos 平台中的核心概念,代表一个业务实体(如客户、订单、产品等)。 + +### 文件格式 + +```yaml +# objects/object_name.object.yml +name: object_name +label: 对象标签 +icon: account +description: 对象描述 +enable_search: true +enable_files: true +enable_tasks: true +enable_notes: true +enable_api: true +enable_share: true +enable_audit: true +version: 2 +``` + +### 主要属性 + +| 属性 | 类型 | 必填 | 说明 | +|------|------|------|------| +| name | String | 是 | 对象API名称,唯一标识符 | +| label | String | 是 | 对象显示名称 | +| icon | String | 否 | 对象图标 | +| description | String | 否 | 对象描述 | +| enable_search | Boolean | 否 | 是否启用搜索,默认 false | +| enable_files | Boolean | 否 | 是否启用附件,默认 false | +| enable_tasks | Boolean | 否 | 是否启用任务,默认 false | +| enable_notes | Boolean | 否 | 是否启用备注,默认 false | +| enable_api | Boolean | 否 | 是否启用API,默认 true | +| enable_share | Boolean | 否 | 是否启用共享,默认 false | +| enable_audit | Boolean | 否 | 是否启用审计,默认 false | +| enable_enhanced_lookup | Boolean | 否 | 是否启用增强查找,默认 false | +| version | Number | 否 | 版本号,默认 2 | + +### 对象示例 + +```yaml +name: sales_order +label: 销售订单 +icon: orders +description: 管理销售订单信息 +enable_search: true +enable_files: true +enable_tasks: true +enable_api: true +enable_share: true +enable_audit: true +version: 2 +fields: + order_number: + type: autonumber + label: 订单号 + formula: "SO-{0000}" + customer: + type: lookup + label: 客户 + reference_to: accounts + required: true + total_amount: + type: currency + label: 总金额 + summary_type: sum + status: + type: select + label: 状态 + options: + - label: 草稿 + value: draft + - label: 已提交 + value: submitted + - label: 已完成 + value: completed + default_value: draft +list_views: + all: + label: 所有订单 + columns: [order_number, customer, total_amount, status, created] + filter_scope: space + filters: [] + my_orders: + label: 我的订单 + columns: [order_number, customer, total_amount, status, created] + filter_scope: mine + filters: [] +``` + +## Applications (应用) + +应用是对象的集合,用于组织相关的业务功能。 + +### 文件格式 + +```yaml +# applications/app_name.app.yml +_id: app_name +name: 应用名称 +description: 应用描述 +icon: apps +is_creator: true +visible: true +sort: 100 +objects: + - object1 + - object2 +``` + +### 主要属性 + +| 属性 | 类型 | 必填 | 说明 | +|------|------|------|------| +| _id | String | 是 | 应用唯一标识符 | +| name | String | 是 | 应用名称 | +| description | String | 否 | 应用描述 | +| icon | String | 否 | 应用图标 | +| is_creator | Boolean | 否 | 是否显示在创建菜单,默认 false | +| visible | Boolean | 否 | 是否可见,默认 true | +| sort | Number | 否 | 排序号 | +| objects | Array | 否 | 包含的对象列表 | + +### 应用示例 + +```yaml +_id: sales +name: 销售管理 +description: 完整的销售管理解决方案 +icon: apps +is_creator: true +visible: true +sort: 100 +objects: + - accounts + - contacts + - sales_order + - products +``` + +## Layouts (页面布局) + +页面布局定义对象详情页面的显示方式。 + +### 文件格式 + +```yaml +# layouts/object_layout.layout.yml +name: layout_name +object_name: object_name +profiles: + - user + - admin +sections: + - label: 基本信息 + columns: 2 + fields: + - field1 + - field2 +``` + +### 主要属性 + +| 属性 | 类型 | 必填 | 说明 | +|------|------|------|------| +| name | String | 是 | 布局名称 | +| object_name | String | 是 | 关联的对象名称 | +| profiles | Array | 否 | 适用的配置文件 | +| sections | Array | 是 | 页面区块定义 | + +### 布局示例 + +```yaml +name: sales_order_default +object_name: sales_order +profiles: + - user + - admin +sections: + - label: 基本信息 + columns: 2 + fields: + - order_number + - customer + - order_date + - status + - label: 金额信息 + columns: 2 + fields: + - subtotal + - tax + - total_amount + - label: 订单明细 + columns: 1 + fields: + - order_items +``` + +## Permission Sets (权限集) + +权限集定义用户对对象和字段的访问权限。 + +### 文件格式 + +```yaml +# permissionsets/permissionset_name.permissionset.yml +name: permissionset_name +label: 权限集标签 +license: platform +object_permissions: + object_name: + allowCreate: true + allowDelete: false + allowEdit: true + allowRead: true + modifyAllRecords: false + viewAllRecords: false +field_permissions: + object_name.field_name: + readable: true + editable: false +``` + +### 主要属性 + +| 属性 | 类型 | 必填 | 说明 | +|------|------|------|------| +| name | String | 是 | 权限集名称 | +| label | String | 是 | 权限集标签 | +| license | String | 否 | 许可证类型 | +| object_permissions | Object | 否 | 对象权限配置 | +| field_permissions | Object | 否 | 字段权限配置 | + +### 权限示例 + +```yaml +name: sales_manager +label: 销售经理 +license: platform +object_permissions: + sales_order: + allowCreate: true + allowDelete: false + allowEdit: true + allowRead: true + modifyAllRecords: true + viewAllRecords: true + accounts: + allowCreate: true + allowDelete: false + allowEdit: true + allowRead: true + modifyAllRecords: false + viewAllRecords: true +field_permissions: + sales_order.total_amount: + readable: true + editable: false + sales_order.discount: + readable: true + editable: true +``` + +## Tabs (标签页) + +标签页定义导航栏中的标签。 + +### 文件格式 + +```yaml +# tabs/tab_name.tab.yml +name: tab_name +label: 标签名称 +icon: custom1 +type: object +object: object_name +sort: 100 +``` + +### 主要属性 + +| 属性 | 类型 | 必填 | 说明 | +|------|------|------|------| +| name | String | 是 | 标签名称 | +| label | String | 是 | 标签显示名称 | +| icon | String | 否 | 标签图标 | +| type | String | 是 | 标签类型 (object/url/page) | +| object | String | 否 | 关联对象(type=object时) | +| url | String | 否 | URL地址(type=url时) | +| sort | Number | 否 | 排序号 | + +## Triggers (触发器) + +触发器定义在记录操作前后执行的业务逻辑。 + +### 文件格式 + +```javascript +// triggers/object_trigger.trigger.js +module.exports = { + listenTo: 'object_name', + + beforeInsert: async function() { + const { doc } = this; + // 插入前逻辑 + }, + + afterInsert: async function() { + const { doc, id } = this; + // 插入后逻辑 + }, + + beforeUpdate: async function() { + const { doc, previousDoc } = this; + // 更新前逻辑 + }, + + afterUpdate: async function() { + const { doc, previousDoc, id } = this; + // 更新后逻辑 + }, + + beforeDelete: async function() { + const { previousDoc, id } = this; + // 删除前逻辑 + }, + + afterDelete: async function() { + const { previousDoc, id } = this; + // 删除后逻辑 + } +}; +``` + +详细说明请参阅 [触发器文档](../triggers/)。 + +## 元数据文件命名规范 + +| 元数据类型 | 文件命名规范 | 示例 | +|-----------|-------------|------| +| Object | `{object_name}.object.yml` | `accounts.object.yml` | +| Application | `{app_name}.app.yml` | `sales.app.yml` | +| Layout | `{layout_name}.layout.yml` | `account_default.layout.yml` | +| Permission Set | `{permissionset_name}.permissionset.yml` | `admin.permissionset.yml` | +| Tab | `{tab_name}.tab.yml` | `accounts_tab.tab.yml` | +| Trigger | `{trigger_name}.trigger.js` | `account_validation.trigger.js` | + +## 元数据加载顺序 + +系统按以下顺序加载元数据: + +1. **Objects** - 对象定义 +2. **Fields** - 字段定义 +3. **Applications** - 应用定义 +4. **Tabs** - 标签页定义 +5. **Layouts** - 页面布局 +6. **Permission Sets** - 权限集 +7. **Triggers** - 触发器 + +## 相关文档 + +- [对象元数据详细说明](./object-metadata.md) +- [字段类型完整参考](./field-types.md) +- [元数据继承规则](./inheritance-rules.md) +- [权限配置](./permissions.md) diff --git a/docs/metadata/object-metadata.md b/docs/metadata/object-metadata.md new file mode 100644 index 0000000000..5baf92fdcf --- /dev/null +++ b/docs/metadata/object-metadata.md @@ -0,0 +1,685 @@ +# 对象元数据详细说明 + +## 概述 + +对象 (Object) 是 Steedos 平台的核心概念,代表一个业务实体,如客户、订单、产品等。每个对象都有完整的元数据定义,包括字段、视图、权限等。 + +## 对象文件结构 + +### 基本文件结构 + +``` +objects/ +└── object_name/ + ├── object_name.object.yml # 对象主定义文件 + ├── fields/ # 字段定义目录(可选) + │ ├── field1.field.yml + │ └── field2.field.yml + ├── buttons/ # 按钮定义目录(可选) + │ └── custom_button.button.yml + ├── actions/ # 操作定义目录(可选) + │ └── custom_action.action.js + └── listviews/ # 列表视图目录(可选) + └── custom_view.listview.yml +``` + +### 简化结构 + +所有定义也可以放在一个文件中: + +```yaml +# objects/object_name.object.yml +name: object_name +label: 对象标签 +fields: + field1: {...} + field2: {...} +list_views: + view1: {...} + view2: {...} +actions: + action1: {...} +``` + +## 对象主要属性 + +### 基本属性 + +```yaml +name: object_name # API 名称(必填) +label: 对象显示名称 # 显示名称(必填) +icon: account # 图标名称 +description: 对象描述 # 描述 +table_name: custom_table # 数据库表名(可选) +datasource: default # 数据源名称(可选) +``` + +### 功能开关 + +```yaml +enable_search: true # 启用全局搜索 +enable_files: true # 启用附件 +enable_tasks: true # 启用任务 +enable_notes: true # 启用备注 +enable_events: true # 启用事件 +enable_api: true # 启用 API 访问 +enable_share: true # 启用共享 +enable_chatter: true # 启用协作 +enable_audit: true # 启用字段历史跟踪 +enable_trash: true # 启用回收站 +enable_space_global: false # 是否为工作区全局对象 +enable_enhanced_lookup: true # 启用增强查找 +enable_inline_edit: true # 启用行内编辑 +enable_instances: false # 启用审批实例 +enable_workflow: false # 启用工作流 +``` + +### 界面配置 + +```yaml +is_view: false # 是否为视图对象 +sidebar: null # 侧边栏配置 +calendar: null # 日历视图配置 +enable_tree: false # 启用树形视图 +parent_field: null # 父字段(树形视图) +children_field: null # 子字段(树形视图) +``` + +### 版本和其他 + +```yaml +version: 2 # 对象版本号 +idFieldName: _id # ID 字段名 +is_enable: true # 是否启用 +in_development: false # 是否处于开发中 +``` + +## 字段定义 + +### 在对象文件中定义字段 + +```yaml +name: custom_object +label: 自定义对象 +fields: + # 文本字段 + name: + type: text + label: 名称 + required: true + searchable: true + + # 数字字段 + amount: + type: currency + label: 金额 + scale: 2 + + # 关系字段 + account: + type: lookup + label: 客户 + reference_to: accounts + required: true + + # 选择字段 + status: + type: select + label: 状态 + options: + - label: 草稿 + value: draft + - label: 已提交 + value: submitted + defaultValue: draft +``` + +### 独立字段文件 + +```yaml +# objects/custom_object/fields/priority.field.yml +name: priority +type: select +label: 优先级 +options: + - label: 高 + value: high + - label: 中 + value: medium + - label: 低 + value: low +defaultValue: medium +``` + +## 列表视图定义 + +### 基本列表视图 + +```yaml +list_views: + all: + label: 所有记录 + columns: # 显示的列 + - name + - status + - owner + - created + filter_scope: space # 过滤范围:space, mine, queue + filters: [] # 过滤条件 + sort: [["created", "desc"]] # 排序规则 + + my_records: + label: 我的记录 + columns: + - name + - status + - modified + filter_scope: mine + filters: [] + + high_priority: + label: 高优先级 + columns: + - name + - priority + - due_date + filter_scope: space + filters: [["priority", "=", "high"]] + sort: [["due_date", "asc"]] +``` + +### 列表视图属性 + +| 属性 | 类型 | 说明 | +|------|------|------| +| label | String | 视图名称 | +| columns | Array | 显示的字段列表 | +| filter_scope | String | 过滤范围:space/mine/queue | +| filters | Array | 过滤条件,使用 ObjectQL 语法 | +| sort | Array | 排序规则 | +| type | String | 视图类型:grid/kanban/calendar | +| mobile_columns | Array | 移动端显示列 | +| extra_columns | Array | 额外的查询字段 | +| options | Object | 其他选项 | + +## 权限配置 + +### 对象级权限 + +```yaml +permission_set: + user: + allowCreate: true # 允许创建 + allowRead: true # 允许读取 + allowEdit: true # 允许编辑 + allowDelete: false # 允许删除 + viewAllRecords: false # 查看所有记录 + modifyAllRecords: false # 修改所有记录 + + admin: + allowCreate: true + allowRead: true + allowEdit: true + allowDelete: true + viewAllRecords: true + modifyAllRecords: true +``` + +### 字段级权限 + +在独立的权限集文件中定义: + +```yaml +# permissionsets/sales_user.permissionset.yml +name: sales_user +label: 销售用户 +field_permissions: + custom_object.amount: + readable: true + editable: false + custom_object.status: + readable: true + editable: true +``` + +## 触发器 + +### 对象触发器 + +```javascript +// triggers/custom_object.trigger.js +module.exports = { + listenTo: 'custom_object', + + // 插入前触发 + beforeInsert: async function() { + const { doc } = this; + + // 自动生成编号 + if (!doc.code) { + doc.code = await generateCode('CO'); + } + + // 数据验证 + if (doc.amount < 0) { + throw new Error('金额不能为负数'); + } + }, + + // 插入后触发 + afterInsert: async function() { + const { doc, id } = this; + + // 发送通知 + await sendNotification({ + to: doc.owner, + message: `新记录已创建: ${doc.name}` + }); + }, + + // 更新前触发 + beforeUpdate: async function() { + const { doc, previousDoc } = this; + + // 状态变更验证 + if (doc.status !== previousDoc.status) { + validateStatusChange(previousDoc.status, doc.status); + } + }, + + // 更新后触发 + afterUpdate: async function() { + const { doc, previousDoc, id } = this; + + // 同步相关数据 + if (doc.account !== previousDoc.account) { + await syncRelatedRecords(id); + } + }, + + // 删除前触发 + beforeDelete: async function() { + const { id, previousDoc } = this; + + // 检查关联记录 + const relatedCount = await checkRelatedRecords(id); + if (relatedCount > 0) { + throw new Error('存在关联记录,无法删除'); + } + }, + + // 删除后触发 + afterDelete: async function() { + const { previousDoc } = this; + + // 清理相关数据 + await cleanupRelatedData(previousDoc); + } +}; +``` + +## 自定义操作 + +### 记录操作 + +```yaml +# objects/custom_object/actions/approve.action.yml +name: approve +label: 批准 +visible: true +on: record +todo: script +script: |- + async function approve() { + const recordId = this.record._id; + await this.object.update(recordId, { + status: 'approved', + approved_by: this.userId, + approved_at: new Date() + }); + toastr.success('已批准'); + } + approve(); +``` + +### 列表操作 + +```yaml +# objects/custom_object/actions/batch_update.action.yml +name: batch_update +label: 批量更新 +visible: true +on: list_item +todo: script +script: |- + async function batchUpdate() { + const selectedRecords = this.selectedRecords; + for (const record of selectedRecords) { + await this.object.update(record._id, { + status: 'updated' + }); + } + toastr.success('批量更新完成'); + } + batchUpdate(); +``` + +## 自定义按钮 + +```yaml +# objects/custom_object/buttons/export.button.yml +name: export +label: 导出 +is_enable: true +visible: true +on: list +todo: script +script: |- + async function exportData() { + const filters = this.filters; + const records = await this.object.find({ filters }); + // 导出逻辑 + downloadCSV(records); + } + exportData(); +``` + +## 关联关系 + +### Lookup 关系 + +```yaml +fields: + account: + type: lookup + label: 客户 + reference_to: accounts + required: true + + contacts: + type: lookup + label: 联系人 + reference_to: contacts + multiple: true + filters: [['account', '=', '{account}']] +``` + +### Master-Detail 关系 + +```yaml +fields: + order: + type: master_detail + label: 订单 + reference_to: sales_orders + required: true +``` + +### 查找过滤器 + +```yaml +fields: + product: + type: lookup + label: 产品 + reference_to: products + filters: [['category', '=', '{category}']] + depend_on: + - category +``` + +## 汇总字段 + +```yaml +fields: + # 在主对象中定义汇总字段 + total_orders: + type: summary + label: 订单总数 + summary_object: sales_orders + summary_type: count + filters: [['account', '=', '{_id}']] + + total_revenue: + type: summary + label: 总收入 + summary_object: sales_orders + summary_type: sum + summary_field: amount + filters: [ + ['account', '=', '{_id}'], + ['status', '=', 'completed'] + ] +``` + +## 公式字段 + +```yaml +fields: + # 文本公式 + full_name: + type: formula + label: 全名 + formula: "first_name + ' ' + last_name" + data_type: text + + # 数值公式 + total_amount: + type: formula + label: 总金额 + formula: "quantity * unit_price * (1 - discount / 100)" + data_type: currency + scale: 2 + + # 日期公式 + days_overdue: + type: formula + label: 逾期天数 + formula: "DATEDIF(due_date, TODAY(), 'D')" + data_type: number +``` + +## 数据验证规则 + +```yaml +# 通过触发器实现 +# triggers/custom_object_validation.trigger.js +module.exports = { + listenTo: 'custom_object', + + beforeInsert: async function() { + const { doc } = this; + + // 验证规则 1:金额范围 + if (doc.amount < 0 || doc.amount > 1000000) { + throw new Error('金额必须在 0 到 1,000,000 之间'); + } + + // 验证规则 2:日期逻辑 + if (doc.end_date < doc.start_date) { + throw new Error('结束日期不能早于开始日期'); + } + + // 验证规则 3:必填条件 + if (doc.status === 'approved' && !doc.approved_by) { + throw new Error('批准状态必须指定批准人'); + } + }, + + beforeUpdate: async function() { + // 更新时的验证规则 + } +}; +``` + +## 对象继承 + +### 扩展标准对象 + +```yaml +# 基础包中的 accounts.object.yml +name: accounts +label: 客户 +fields: + name: + type: text + label: 名称 + phone: + type: text + label: 电话 +``` + +```yaml +# 扩展包中的 accounts.object.yml +name: accounts +fields: + # 添加新字段 + industry: + type: select + label: 行业 + options: + - label: IT + value: it + - label: 制造 + value: manufacturing + + # 修改已有字段 + phone: + required: true + label: 联系电话 +``` + +## 完整示例 + +```yaml +name: sales_order +label: 销售订单 +icon: orders +description: 管理销售订单 +enable_search: true +enable_files: true +enable_tasks: true +enable_api: true +enable_audit: true +version: 2 + +fields: + # 自动编号 + order_number: + type: autonumber + label: 订单号 + formula: "SO-{YYYY}{MM}{DD}-{0000}" + readonly: true + + # 查找关系 + account: + type: lookup + label: 客户 + reference_to: accounts + required: true + + contact: + type: lookup + label: 联系人 + reference_to: contacts + filters: [['account', '=', '{account}']] + depend_on: + - account + + # 日期 + order_date: + type: date + label: 订单日期 + required: true + defaultValue: "{now}" + + # 选择 + status: + type: select + label: 状态 + options: + - label: 草稿 + value: draft + - label: 已提交 + value: submitted + - label: 已批准 + value: approved + - label: 已完成 + value: completed + defaultValue: draft + + # 子表 + order_items: + type: grid + label: 订单明细 + is_wide: true + + # 货币 + subtotal: + type: currency + label: 小计 + scale: 2 + + tax: + type: currency + label: 税额 + scale: 2 + + # 公式 + total_amount: + type: formula + label: 总金额 + formula: "subtotal + tax" + data_type: currency + scale: 2 + +list_views: + all: + label: 所有订单 + columns: + - order_number + - account + - order_date + - status + - total_amount + - owner + filter_scope: space + filters: [] + sort: [["order_date", "desc"]] + + pending: + label: 待处理 + columns: + - order_number + - account + - order_date + - total_amount + filter_scope: space + filters: [["status", "in", ["draft", "submitted"]]] + sort: [["order_date", "asc"]] + +permission_set: + user: + allowCreate: true + allowRead: true + allowEdit: true + allowDelete: false + viewAllRecords: false + modifyAllRecords: false + + admin: + allowCreate: true + allowRead: true + allowEdit: true + allowDelete: true + viewAllRecords: true + modifyAllRecords: true +``` + +## 相关文档 + +- [元数据概述](./README.md) +- [字段类型参考](./field-types.md) +- [元数据继承规则](./inheritance-rules.md) +- [触发器开发](../triggers/) +- [ObjectQL 查询](../objectql/) diff --git a/docs/metadata/permissions.md b/docs/metadata/permissions.md new file mode 100644 index 0000000000..84b32a8630 --- /dev/null +++ b/docs/metadata/permissions.md @@ -0,0 +1,578 @@ +# 权限配置详细说明 + +## 概述 + +Steedos 平台提供灵活的权限管理机制,支持对象级权限、字段级权限、记录级权限等多个层次的权限控制。 + +## 权限层次 + +``` +用户 (User) + ↓ +简档 (Profile) / 权限集 (Permission Set) + ↓ +对象权限 (Object Permissions) + ↓ +字段权限 (Field Permissions) + ↓ +记录权限 (Record Permissions) +``` + +## 权限集 (Permission Set) + +### 权限集文件格式 + +```yaml +# permissionsets/permissionset_name.permissionset.yml +name: permissionset_name +label: 权限集标签 +license: platform +object_permissions: + object_name: + allowCreate: true + allowRead: true + allowEdit: true + allowDelete: true + viewAllRecords: false + modifyAllRecords: false +field_permissions: + object_name.field_name: + readable: true + editable: false +``` + +### 权限集属性 + +| 属性 | 类型 | 说明 | +|------|------|------| +| name | String | 权限集 API 名称 | +| label | String | 权限集显示名称 | +| license | String | 许可证类型 | +| object_permissions | Object | 对象权限配置 | +| field_permissions | Object | 字段权限配置 | + +## 对象权限 + +### 对象权限类型 + +```yaml +object_permissions: + sales_order: + # 基本权限 + allowCreate: true # 创建权限 + allowRead: true # 读取权限 + allowEdit: true # 编辑权限 + allowDelete: false # 删除权限 + + # 扩展权限 + viewAllRecords: false # 查看所有记录 + modifyAllRecords: false # 修改所有记录 + + # 其他权限 + allowCreateEdit: true # 创建和编辑 + allowReadEdit: true # 读取和编辑 + viewCompanyRecords: false # 查看分部记录 + modifyCompanyRecords: false # 修改分部记录 +``` + +### 权限说明 + +| 权限 | 说明 | 影响范围 | +|------|------|---------| +| allowCreate | 允许创建新记录 | 用户可以创建新记录 | +| allowRead | 允许读取记录 | 用户可以查看记录 | +| allowEdit | 允许编辑记录 | 用户可以编辑自己拥有的记录 | +| allowDelete | 允许删除记录 | 用户可以删除自己拥有的记录 | +| viewAllRecords | 查看所有记录 | 用户可以查看所有记录,不受所有者限制 | +| modifyAllRecords | 修改所有记录 | 用户可以编辑和删除所有记录 | + +### 对象权限示例 + +#### 基本用户权限 + +```yaml +name: standard_user +label: 标准用户 +object_permissions: + accounts: + allowCreate: true + allowRead: true + allowEdit: true + allowDelete: false + viewAllRecords: false # 只能看自己的记录 + modifyAllRecords: false + + contacts: + allowCreate: true + allowRead: true + allowEdit: true + allowDelete: false + viewAllRecords: false + modifyAllRecords: false +``` + +#### 管理员权限 + +```yaml +name: system_admin +label: 系统管理员 +object_permissions: + accounts: + allowCreate: true + allowRead: true + allowEdit: true + allowDelete: true + viewAllRecords: true # 可以看所有记录 + modifyAllRecords: true # 可以修改所有记录 + + contacts: + allowCreate: true + allowRead: true + allowEdit: true + allowDelete: true + viewAllRecords: true + modifyAllRecords: true +``` + +#### 只读用户权限 + +```yaml +name: read_only_user +label: 只读用户 +object_permissions: + accounts: + allowCreate: false + allowRead: true + allowEdit: false + allowDelete: false + viewAllRecords: true # 可以看所有记录 + modifyAllRecords: false + + reports: + allowCreate: false + allowRead: true + allowEdit: false + allowDelete: false + viewAllRecords: true + modifyAllRecords: false +``` + +## 字段权限 + +### 字段权限配置 + +```yaml +field_permissions: + # 格式:object_name.field_name + accounts.revenue: + readable: true # 可读 + editable: true # 可编辑 + + accounts.credit_limit: + readable: true + editable: false # 只读 + + accounts.internal_notes: + readable: false # 不可见 + editable: false +``` + +### 字段权限说明 + +| 权限 | 说明 | +|------|------| +| readable: true | 字段可见,用户可以查看字段值 | +| readable: false | 字段不可见,用户看不到字段 | +| editable: true | 字段可编辑,用户可以修改字段值 | +| editable: false | 字段只读,用户可以看到但不能修改 | + +### 字段权限示例 + +#### 销售人员权限 + +```yaml +name: sales_representative +label: 销售代表 +field_permissions: + # 客户对象 + accounts.name: + readable: true + editable: true + + accounts.revenue: + readable: true + editable: false # 只读,不能修改收入 + + accounts.credit_rating: + readable: false # 不可见 + editable: false + + # 订单对象 + sales_orders.amount: + readable: true + editable: true + + sales_orders.discount: + readable: true + editable: false # 不能修改折扣 + + sales_orders.cost: + readable: false # 看不到成本 + editable: false +``` + +#### 财务人员权限 + +```yaml +name: finance_user +label: 财务人员 +field_permissions: + accounts.revenue: + readable: true + editable: true # 可以修改收入 + + accounts.credit_limit: + readable: true + editable: true + + accounts.credit_rating: + readable: true + editable: true # 可以看到和修改信用评级 + + sales_orders.cost: + readable: true # 可以看到成本 + editable: true + + sales_orders.discount: + readable: true + editable: true # 可以修改折扣 +``` + +## 记录级权限 + +### 所有者规则 + +默认情况下,用户可以访问: +1. 自己创建的记录(owner = 当前用户) +2. 共享给自己的记录 +3. 所属下级用户创建的记录(如果启用角色层次) + +### 共享规则 + +#### 手动共享 + +```javascript +// 通过 API 共享记录 +await broker.call('objectql.shareRecord', { + objectName: 'accounts', + recordId: 'xxx', + userIds: ['user1', 'user2'], + shareType: 'read' // read, edit +}); +``` + +#### 共享规则配置 + +```yaml +# 在对象中定义共享规则 +name: accounts +enable_share: true # 启用共享功能 + +sharing_rules: + - name: team_sharing + label: 团队共享 + shared_to: + type: users + users: ['{manager}'] + access_level: read +``` + +### 角色层次 + +通过角色层次,上级可以访问下级的记录: + +``` +销售总监 + ↓ +销售经理 + ↓ +销售代表 +``` + +销售总监可以看到销售经理和销售代表的记录。 + +## 权限继承 + +### 权限集继承 + +```yaml +# 基础权限集 +name: base_user +object_permissions: + accounts: + allowCreate: true + allowRead: true +``` + +```yaml +# 扩展权限集(在用户上叠加多个权限集) +name: sales_permissions +object_permissions: + sales_orders: + allowCreate: true + allowRead: true + allowEdit: true +``` + +用户可以拥有多个权限集,权限取并集(最大权限)。 + +### 对象内权限继承 + +在对象定义中配置默认权限: + +```yaml +name: custom_object +permission_set: + user: + allowCreate: true + allowRead: true + allowEdit: true + allowDelete: false + + admin: + allowCreate: true + allowRead: true + allowEdit: true + allowDelete: true + viewAllRecords: true + modifyAllRecords: true +``` + +## 权限计算规则 + +### 权限组合规则 + +用户的最终权限是以下几个来源的组合: + +1. **Profile(简档)** - 基础权限 +2. **Permission Sets(权限集)** - 附加权限 +3. **Object Settings(对象设置)** - 对象级默认权限 +4. **Sharing Rules(共享规则)** - 记录级权限 +5. **Manual Sharing(手动共享)** - 单独共享 + +### 权限计算优先级 + +``` +最严格(Least Privileged) + ↓ +Profile 权限 + ↓ +Permission Set 权限(取并集) + ↓ +Object Permission(取并集) + ↓ +Record Sharing(取并集) + ↓ +最宽松(Most Privileged) +``` + +### 权限计算示例 + +**Profile 权限**: +```yaml +accounts: + allowCreate: true + allowRead: true + allowEdit: false + allowDelete: false +``` + +**Permission Set 权限**: +```yaml +accounts: + allowEdit: true # 增加编辑权限 +``` + +**最终权限**(取并集): +```yaml +accounts: + allowCreate: true + allowRead: true + allowEdit: true # ✓ 从 Permission Set 获得 + allowDelete: false +``` + +## 权限验证 + +### API 权限验证 + +```javascript +// 通过 userSession 参数启用权限验证 +const records = await objects.accounts.find( + { + filters: [['status', '=', 'active']] + }, + userSession // 传入用户会话 +); +``` + +### 触发器中验证权限 + +```javascript +module.exports = { + beforeUpdate: async function() { + const { userId, spaceId } = this; + + // 检查用户权限 + const hasPermission = await checkUserPermission( + userId, + spaceId, + 'accounts', + 'edit' + ); + + if (!hasPermission) { + throw new Error('没有编辑权限'); + } + } +}; +``` + +## 特殊权限场景 + +### 批准流程权限 + +```yaml +name: approver +label: 批准人 +object_permissions: + approval_requests: + allowCreate: false + allowRead: true + allowEdit: true # 可以批准/拒绝 + allowDelete: false +``` + +### 报表查看权限 + +```yaml +name: report_viewer +label: 报表查看者 +object_permissions: + reports: + allowCreate: false + allowRead: true + allowEdit: false + allowDelete: false + viewAllRecords: true # 可以查看所有报表 +``` + +### 临时权限提升 + +```javascript +// 在代码中使用管理员权限执行操作 +const adminSession = { + userId: 'admin', + spaceId: spaceId, + is_space_admin: true +}; + +await objects.accounts.update( + recordId, + { status: 'approved' }, + adminSession +); +``` + +## 权限最佳实践 + +### 1. 最小权限原则 + +只授予用户完成工作所需的最小权限: + +```yaml +# 推荐:最小权限 +name: data_entry +object_permissions: + accounts: + allowCreate: true + allowRead: true + allowEdit: true + allowDelete: false # 不授予删除权限 + viewAllRecords: false +``` + +### 2. 使用权限集分层 + +```yaml +# 基础权限集 +name: base_sales + +# 专项权限集 +name: discount_approval +name: price_override +``` + +### 3. 敏感字段权限控制 + +```yaml +field_permissions: + accounts.bank_account: + readable: false + editable: false + + accounts.tax_id: + readable: true + editable: false # 只读 +``` + +### 4. 定期审查权限 + +- 定期检查用户权限配置 +- 删除不再需要的权限集 +- 审查具有高权限的用户 + +### 5. 文档化权限规则 + +在权限集文件中添加注释: + +```yaml +# 销售经理权限集 +# 用途:授予销售经理管理销售数据的权限 +# 适用对象:销售经理及以上级别 +name: sales_manager +label: 销售经理 +``` + +## 常见问题 + +### Q: 用户看不到某些记录? + +A: 检查以下几点: +1. 对象权限是否包含 allowRead +2. 是否需要 viewAllRecords 权限 +3. 记录是否共享给该用户 +4. 是否配置了过滤规则 + +### Q: 字段不可编辑? + +A: 检查: +1. 字段权限 editable 设置 +2. 字段是否设置为 readonly +3. 对象权限是否包含 allowEdit +4. 记录级权限是否允许编辑 + +### Q: 如何临时提升权限? + +A: 在代码中使用管理员会话: +```javascript +const adminSession = { is_space_admin: true }; +await objects.xxx.operation({...}, adminSession); +``` + +## 相关文档 + +- [元数据概述](./README.md) +- [对象元数据](./object-metadata.md) +- [元数据类型](./metadata-types.md) +- [用户和组织管理](../DEVELOPER_GUIDE.md) diff --git a/docs/objectql.md b/docs/objectql/README.md similarity index 100% rename from docs/objectql.md rename to docs/objectql/README.md diff --git a/docs/objectql/best-practices.md b/docs/objectql/best-practices.md new file mode 100644 index 0000000000..36fe1e83ea --- /dev/null +++ b/docs/objectql/best-practices.md @@ -0,0 +1,603 @@ +# ObjectQL 最佳实践 + +## 概述 + +本文档提供使用 ObjectQL 的最佳实践和建议,帮助您编写高效、可维护的代码。 + +## 查询优化 + +### 1. 只查询需要的字段 + +**不推荐** ❌: +```javascript +// 查询所有字段 +const records = await objects.accounts.find({ + filters: [['status', '=', 'active']] +}); +``` + +**推荐** ✅: +```javascript +// 只查询需要的字段 +const records = await objects.accounts.find({ + fields: ['name', 'phone', 'email'], + filters: [['status', '=', 'active']] +}); +``` + +**原因**: 减少数据传输量,提高查询速度。 + +### 2. 使用 top 限制返回数量 + +**不推荐** ❌: +```javascript +// 不限制返回数量,可能返回大量数据 +const records = await objects.accounts.find({ + filters: [['status', '=', 'active']] +}); +``` + +**推荐** ✅: +```javascript +// 限制返回数量 +const records = await objects.accounts.find({ + filters: [['status', '=', 'active']], + top: 100 +}); +``` + +**原因**: 防止一次性加载过多数据,影响性能和内存使用。 + +### 3. 在索引字段上建立过滤条件 + +**不推荐** ❌: +```javascript +// 在非索引字段上过滤 +const records = await objects.accounts.find({ + filters: [['description', 'contains', '关键词']] +}); +``` + +**推荐** ✅: +```javascript +// 优先在索引字段上过滤 +const records = await objects.accounts.find({ + filters: [ + ['status', '=', 'active'], // 索引字段 + 'and', + ['category', '=', 'important'] // 索引字段 + ] +}); +``` + +**原因**: 索引字段查询速度更快。 + +### 4. 使用 IN 代替多个 OR 条件 + +**不推荐** ❌: +```javascript +const records = await objects.accounts.find({ + filters: [ + ['status', '=', 'active'], + 'or', + ['status', '=', 'pending'], + 'or', + ['status', '=', 'approved'] + ] +}); +``` + +**推荐** ✅: +```javascript +const records = await objects.accounts.find({ + filters: [['status', 'in', ['active', 'pending', 'approved']]] +}); +``` + +**原因**: 更简洁,性能更好。 + +### 5. 合理使用分页 + +**推荐** ✅: +```javascript +async function getPaginatedData(pageNumber = 1, pageSize = 20) { + const skip = (pageNumber - 1) * pageSize; + + const records = await objects.accounts.find({ + fields: ['name', 'status', 'created'], + filters: [['status', '=', 'active']], + sort: 'created desc', + top: pageSize, + skip: skip + }); + + return records; +} +``` + +**原因**: 避免一次性加载所有数据。 + +## 权限和安全 + +### 1. 始终传递 userSession + +**不推荐** ❌: +```javascript +// 不传递 userSession,绕过权限检查 +const records = await objects.accounts.find({ + filters: [['status', '=', 'active']] +}); +``` + +**推荐** ✅: +```javascript +// 传递 userSession 进行权限验证 +const records = await objects.accounts.find({ + filters: [['status', '=', 'active']] +}, userSession); +``` + +**原因**: 确保数据安全,只返回用户有权访问的数据。 + +### 2. 在触发器中验证权限 + +**推荐** ✅: +```javascript +module.exports = { + beforeUpdate: async function() { + const { userId, spaceId, doc, previousDoc } = this; + + // 验证用户是否有权修改关键字段 + if (doc.status !== previousDoc.status) { + const hasPermission = await checkApprovalPermission(userId, spaceId); + if (!hasPermission) { + throw new Error('您没有权限修改状态'); + } + } + } +}; +``` + +### 3. 过滤敏感数据 + +**推荐** ✅: +```javascript +// 不返回敏感字段 +const records = await objects.accounts.find({ + fields: ['name', 'phone', 'email'], // 不包含敏感字段 + filters: [['status', '=', 'active']] +}, userSession); +``` + +## 错误处理 + +### 1. 使用 try-catch 处理错误 + +**推荐** ✅: +```javascript +async function getAccount(id) { + try { + const record = await objects.accounts.findOne(id, { + fields: ['name', 'phone', 'email'] + }, userSession); + + if (!record) { + throw new Error('记录不存在'); + } + + return record; + } catch (error) { + console.error('查询失败:', error); + throw error; + } +} +``` + +### 2. 验证查询结果 + +**推荐** ✅: +```javascript +async function updateAccount(id, data) { + // 先查询记录是否存在 + const existing = await objects.accounts.findOne(id, { + fields: ['_id'] + }, userSession); + + if (!existing) { + throw new Error('记录不存在'); + } + + // 执行更新 + return await objects.accounts.update(id, data, userSession); +} +``` + +### 3. 提供有意义的错误消息 + +**不推荐** ❌: +```javascript +if (!record) { + throw new Error('错误'); +} +``` + +**推荐** ✅: +```javascript +if (!record) { + throw new Error('未找到 ID 为 ${id} 的客户记录'); +} +``` + +## 代码组织 + +### 1. 封装常用查询 + +**推荐** ✅: +```javascript +// services/accountService.js +class AccountService { + // 获取活跃客户 + async getActiveAccounts(userSession) { + return await objects.accounts.find({ + fields: ['name', 'phone', 'email', 'status'], + filters: [['status', '=', 'active']], + sort: 'name' + }, userSession); + } + + // 获取高价值客户 + async getHighValueAccounts(userSession) { + return await objects.accounts.find({ + fields: ['name', 'annual_revenue', 'rating'], + filters: [ + ['annual_revenue', '>', 1000000], + 'and', + ['rating', '=', 'A'] + ], + sort: 'annual_revenue desc' + }, userSession); + } + + // 搜索客户 + async searchAccounts(keyword, userSession) { + return await objects.accounts.find({ + fields: ['name', 'phone', 'email'], + filters: [['name', 'contains', keyword]], + top: 50 + }, userSession); + } +} + +module.exports = new AccountService(); +``` + +### 2. 使用常量定义过滤条件 + +**推荐** ✅: +```javascript +// constants/filters.js +const ACCOUNT_FILTERS = { + ACTIVE: [['status', '=', 'active']], + HIGH_VALUE: [['annual_revenue', '>', 1000000]], + VIP: [['customer_type', '=', 'vip']] +}; + +// 使用 +const records = await objects.accounts.find({ + filters: ACCOUNT_FILTERS.ACTIVE +}, userSession); +``` + +### 3. 创建可复用的查询函数 + +**推荐** ✅: +```javascript +// utils/queryHelper.js +async function findByOwner(objectName, userId, fields, userSession) { + return await objects[objectName].find({ + fields: fields, + filters: [['owner', '=', userId]], + sort: 'modified desc' + }, userSession); +} + +// 使用 +const myAccounts = await findByOwner('accounts', userId, ['name', 'status'], userSession); +const myOrders = await findByOwner('sales_orders', userId, ['order_number', 'amount'], userSession); +``` + +## 触发器最佳实践 + +### 1. 保持触发器逻辑简单 + +**不推荐** ❌: +```javascript +module.exports = { + beforeInsert: async function() { + // 复杂的业务逻辑 + // ...大量代码... + } +}; +``` + +**推荐** ✅: +```javascript +module.exports = { + beforeInsert: async function() { + const { doc } = this; + + // 调用外部服务处理复杂逻辑 + await accountService.validateAccount(doc); + await accountService.enrichAccountData(doc); + } +}; +``` + +### 2. 避免在触发器中进行循环查询 + +**不推荐** ❌: +```javascript +module.exports = { + afterInsert: async function() { + const { doc } = this; + + // 为每个联系人单独查询 + for (const contactId of doc.contacts) { + const contact = await objects.contacts.findOne(contactId); + // 处理联系人 + } + } +}; +``` + +**推荐** ✅: +```javascript +module.exports = { + afterInsert: async function() { + const { doc } = this; + + // 一次性查询所有联系人 + const contacts = await objects.contacts.find({ + filters: [['_id', 'in', doc.contacts]] + }); + + // 批量处理 + for (const contact of contacts) { + // 处理联系人 + } + } +}; +``` + +### 3. 使用事务保证数据一致性 + +**推荐** ✅: +```javascript +module.exports = { + afterInsert: async function() { + const { doc, id } = this; + + try { + // 更新相关记录 + await objects.contacts.update(doc.primary_contact, { + account: id + }); + + // 创建关联记录 + await objects.opportunities.insert({ + account: id, + name: `${doc.name} - Initial Opportunity` + }); + } catch (error) { + // 错误处理 + console.error('触发器执行失败:', error); + throw error; + } + } +}; +``` + +## 性能监控 + +### 1. 记录查询时间 + +**推荐** ✅: +```javascript +async function getAccountsWithTiming(filters, userSession) { + const startTime = Date.now(); + + try { + const records = await objects.accounts.find({ + fields: ['name', 'status'], + filters: filters + }, userSession); + + const duration = Date.now() - startTime; + console.log(`查询耗时: ${duration}ms, 返回 ${records.length} 条记录`); + + return records; + } catch (error) { + console.error('查询失败:', error); + throw error; + } +} +``` + +### 2. 添加查询日志 + +**推荐** ✅: +```javascript +async function loggedQuery(objectName, query, userSession) { + console.log(`[ObjectQL] 查询 ${objectName}:`, JSON.stringify(query)); + + const result = await objects[objectName].find(query, userSession); + + console.log(`[ObjectQL] 返回 ${result.length} 条记录`); + + return result; +} +``` + +## 测试 + +### 1. 编写单元测试 + +**推荐** ✅: +```javascript +// tests/accountService.test.js +describe('AccountService', () => { + it('should get active accounts', async () => { + const userSession = createTestUserSession(); + + const accounts = await accountService.getActiveAccounts(userSession); + + expect(accounts).toBeDefined(); + expect(accounts.length).toBeGreaterThan(0); + expect(accounts[0].status).toBe('active'); + }); + + it('should search accounts by name', async () => { + const userSession = createTestUserSession(); + + const accounts = await accountService.searchAccounts('Test', userSession); + + expect(accounts).toBeDefined(); + accounts.forEach(account => { + expect(account.name).toContain('Test'); + }); + }); +}); +``` + +### 2. 模拟 ObjectQL 调用 + +**推荐** ✅: +```javascript +// tests/mocks/objectql.mock.js +const mockObjectQL = { + accounts: { + find: jest.fn(), + findOne: jest.fn(), + insert: jest.fn(), + update: jest.fn(), + delete: jest.fn() + } +}; + +// 在测试中使用 +mockObjectQL.accounts.find.mockResolvedValue([ + { _id: '1', name: 'Test Account', status: 'active' } +]); +``` + +## 常见陷阱 + +### 1. 忘记传递 userSession + +```javascript +// ❌ 错误:绕过权限检查 +const records = await objects.accounts.find({ filters: [...] }); + +// ✅ 正确:包含权限检查 +const records = await objects.accounts.find({ filters: [...] }, userSession); +``` + +### 2. 在循环中进行查询 + +```javascript +// ❌ 错误:N+1 查询问题 +for (const accountId of accountIds) { + const account = await objects.accounts.findOne(accountId); + // 处理... +} + +// ✅ 正确:批量查询 +const accounts = await objects.accounts.find({ + filters: [['_id', 'in', accountIds]] +}); +``` + +### 3. 不处理空结果 + +```javascript +// ❌ 错误:未检查结果 +const record = await objects.accounts.findOne(id); +const name = record.name; // 可能报错 + +// ✅ 正确:检查结果 +const record = await objects.accounts.findOne(id); +if (!record) { + throw new Error('记录不存在'); +} +const name = record.name; +``` + +### 4. 过度使用 viewAllRecords + +```javascript +// ❌ 错误:不必要的全局权限 +const records = await objects.accounts.find({ + filters: [['owner', '=', userId]] // 只查询自己的记录 +}, { ...userSession, viewAllRecords: true }); // 不需要全局权限 + +// ✅ 正确:使用正常权限 +const records = await objects.accounts.find({ + filters: [['owner', '=', userId]] +}, userSession); +``` + +## 文档化 + +### 1. 添加注释说明查询用途 + +**推荐** ✅: +```javascript +/** + * 获取需要跟进的客户列表 + * 条件: + * 1. 状态为活跃 + * 2. 最后联系时间超过 30 天 + * 3. 客户评级为 A 或 B + */ +async function getAccountsNeedingFollowUp(userSession) { + const thirtyDaysAgo = new Date(); + thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30); + + return await objects.accounts.find({ + fields: ['name', 'last_contact_date', 'rating', 'owner'], + filters: [ + ['status', '=', 'active'], + 'and', + ['last_contact_date', '<', thirtyDaysAgo], + 'and', + ['rating', 'in', ['A', 'B']] + ], + sort: 'last_contact_date' + }, userSession); +} +``` + +### 2. 记录复杂过滤条件的业务逻辑 + +**推荐** ✅: +```javascript +// 高价值客户定义: +// - 年收入 > 100万 或 +// - (客户评级 = A 且 订单数 > 10) +const HIGH_VALUE_CUSTOMER_FILTERS = [ + ['annual_revenue', '>', 1000000], + 'or', + [ + ['rating', '=', 'A'], + 'and', + ['order_count', '>', 10] + ] +]; +``` + +## 相关文档 + +- [ObjectQL 概述](./README.md) +- [查询语法详解](./query-syntax.md) +- [过滤器操作符参考](./filter-operators.md) +- [触发器开发](../triggers/) diff --git a/docs/objectql/filter-operators.md b/docs/objectql/filter-operators.md new file mode 100644 index 0000000000..2645716a95 --- /dev/null +++ b/docs/objectql/filter-operators.md @@ -0,0 +1,625 @@ +# ObjectQL 过滤器操作符完整参考 + +## 概述 + +ObjectQL 提供了丰富的过滤器操作符,用于构建灵活的查询条件。所有操作符都支持跨数据库使用(MongoDB、PostgreSQL、MySQL 等)。 + +## 基本操作符 + +### 等于 (=) + +匹配等于指定值的记录。 + +**语法**: +```javascript +[field, '=', value] +``` + +**示例**: +```javascript +// 单个值 +filters: [['status', '=', 'active']] + +// 数组值(IN 查询) +filters: [['status', '=', ['active', 'pending']]] + +// 等同于 +filters: [['status', 'in', ['active', 'pending']]] + +// 空值 +filters: [['description', '=', null]] + +// 布尔值 +filters: [['is_active', '=', true]] + +// 日期 +filters: [['created', '=', '2024-01-01']] +``` + +### 不等于 (!=) + +匹配不等于指定值的记录。 + +**语法**: +```javascript +[field, '!=', value] +``` + +**示例**: +```javascript +// 单个值 +filters: [['status', '!=', 'deleted']] + +// 数组值(NOT IN 查询) +filters: [['status', '!=', ['deleted', 'cancelled']]] + +// 等同于 +filters: [['status', 'not in', ['deleted', 'cancelled']]] + +// 非空 +filters: [['description', '!=', null]] +``` + +## 比较操作符 + +### 大于 (>) + +匹配大于指定值的记录。 + +**语法**: +```javascript +[field, '>', value] +``` + +**示例**: +```javascript +// 数字 +filters: [['amount', '>', 1000]] + +// 日期 +filters: [['created', '>', '2024-01-01']] + +// 日期时间 +filters: [['created', '>', '2024-01-01T00:00:00Z']] +``` + +### 大于等于 (>=) + +匹配大于或等于指定值的记录。 + +**语法**: +```javascript +[field, '>=', value] +``` + +**示例**: +```javascript +filters: [['amount', '>=', 1000]] +filters: [['score', '>=', 60]] +filters: [['created', '>=', '2024-01-01']] +``` + +### 小于 (<) + +匹配小于指定值的记录。 + +**语法**: +```javascript +[field, '<', value] +``` + +**示例**: +```javascript +filters: [['amount', '<', 5000]] +filters: [['age', '<', 30]] +filters: [['due_date', '<', '2024-12-31']] +``` + +### 小于等于 (<=) + +匹配小于或等于指定值的记录。 + +**语法**: +```javascript +[field, '<=', value] +``` + +**示例**: +```javascript +filters: [['amount', '<=', 5000]] +filters: [['score', '<=', 100]] +filters: [['created', '<=', '2024-12-31']] +``` + +## 字符串操作符 + +### 包含 (contains) + +匹配包含指定子字符串的记录。 + +**语法**: +```javascript +[field, 'contains', substring] +``` + +**示例**: +```javascript +// 名称包含"公司" +filters: [['name', 'contains', '公司']] + +// 描述包含"重要" +filters: [['description', 'contains', '重要']] + +// 邮箱包含"@example.com" +filters: [['email', 'contains', '@example.com']] +``` + +**注意**: 区分大小写取决于数据库配置。 + +### 不包含 (notcontains) + +匹配不包含指定子字符串的记录。 + +**语法**: +```javascript +[field, 'notcontains', substring] +``` + +**示例**: +```javascript +// 名称不包含"测试" +filters: [['name', 'notcontains', '测试']] + +// 描述不包含"已删除" +filters: [['description', 'notcontains', '已删除']] +``` + +### 以...开始 (startswith) + +匹配以指定字符串开始的记录。 + +**语法**: +```javascript +[field, 'startswith', prefix] +``` + +**示例**: +```javascript +// 名称以"A"开始 +filters: [['name', 'startswith', 'A']] + +// 订单号以"SO-"开始 +filters: [['order_number', 'startswith', 'SO-']] + +// 邮箱以"admin"开始 +filters: [['email', 'startswith', 'admin']] +``` + +### 以...结束 (endswith) + +匹配以指定字符串结束的记录(部分数据库支持)。 + +**语法**: +```javascript +[field, 'endswith', suffix] +``` + +**示例**: +```javascript +// 名称以"有限公司"结束 +filters: [['name', 'endswith', '有限公司']] + +// 文件名以".pdf"结束 +filters: [['filename', 'endswith', '.pdf']] + +// 邮箱以"@example.com"结束 +filters: [['email', 'endswith', '@example.com']] +``` + +## 范围操作符 + +### 在范围内 (between) + +匹配在指定范围内的记录(包含边界值)。 + +**语法**: +```javascript +[field, 'between', [min, max]] +``` + +**示例**: +```javascript +// 金额在 1000 到 5000 之间 +filters: [['amount', 'between', [1000, 5000]]] + +// 等同于 +filters: [ + ['amount', '>=', 1000], + 'and', + ['amount', '<=', 5000] +] + +// 年龄在 20 到 30 之间 +filters: [['age', 'between', [20, 30]]] + +// 日期范围 +filters: [['created', 'between', ['2024-01-01', '2024-12-31']]] + +// 开放范围(只有下限) +filters: [['age', 'between', [20, null]]] +// 等同于 +filters: [['age', '>=', 20]] + +// 开放范围(只有上限) +filters: [['age', 'between', [null, 30]]] +// 等同于 +filters: [['age', '<=', 30]] +``` + +## 集合操作符 + +### 在列表中 (in) + +匹配值在指定列表中的记录。 + +**语法**: +```javascript +[field, 'in', array] +``` + +**示例**: +```javascript +// 状态是 active、pending 或 approved +filters: [['status', 'in', ['active', 'pending', 'approved']]] + +// 优先级是 high 或 urgent +filters: [['priority', 'in', ['high', 'urgent']]] + +// 等同于使用 = +filters: [['status', '=', ['active', 'pending', 'approved']]] +``` + +### 不在列表中 (not in) + +匹配值不在指定列表中的记录。 + +**语法**: +```javascript +[field, 'not in', array] +``` + +**示例**: +```javascript +// 状态不是 deleted、cancelled 或 archived +filters: [['status', 'not in', ['deleted', 'cancelled', 'archived']]] + +// 等同于使用 != +filters: [['status', '!=', ['deleted', 'cancelled', 'archived']]] +``` + +## 特殊日期操作符 + +ObjectQL 支持特殊的相对日期值,便于动态查询。 + +### 相对日期值 + +```javascript +// 今天 +filters: [['created', '=', 'today']] + +// 昨天 +filters: [['created', '=', 'yesterday']] + +// 明天 +filters: [['created', '=', 'tomorrow']] + +// 本周 +filters: [['created', '=', 'this_week']] + +// 上周 +filters: [['created', '=', 'last_week']] + +// 下周 +filters: [['created', '=', 'next_week']] + +// 本月 +filters: [['created', '=', 'this_month']] + +// 上月 +filters: [['created', '=', 'last_month']] + +// 下月 +filters: [['created', '=', 'next_month']] + +// 本季度 +filters: [['created', '=', 'this_quarter']] + +// 上季度 +filters: [['created', '=', 'last_quarter']] + +// 今年 +filters: [['created', '=', 'this_year']] + +// 去年 +filters: [['created', '=', 'last_year']] + +// 明年 +filters: [['created', '=', 'next_year']] +``` + +### 相对天数范围 + +```javascript +// 过去 7 天 +filters: [['created', '=', 'last_7_days']] + +// 过去 30 天 +filters: [['created', '=', 'last_30_days']] + +// 过去 60 天 +filters: [['created', '=', 'last_60_days']] + +// 过去 90 天 +filters: [['created', '=', 'last_90_days']] + +// 未来 7 天 +filters: [['created', '=', 'next_7_days']] + +// 未来 30 天 +filters: [['created', '=', 'next_30_days']] + +// 未来 60 天 +filters: [['created', '=', 'next_60_days']] + +// 未来 90 天 +filters: [['created', '=', 'next_90_days']] +``` + +## 逻辑操作符 + +### AND 操作符 + +连接多个条件,所有条件都必须满足。 + +**语法**: +```javascript +[condition1, 'and', condition2] +``` + +**示例**: +```javascript +// 默认使用 AND(可省略) +filters: [ + ['status', '=', 'active'], + ['amount', '>', 1000] +] + +// 显式使用 AND +filters: [ + ['status', '=', 'active'], + 'and', + ['amount', '>', 1000] +] + +// 多个 AND 条件 +filters: [ + ['status', '=', 'active'], + 'and', + ['amount', '>', 1000], + 'and', + ['priority', '=', 'high'] +] +``` + +### OR 操作符 + +连接多个条件,任一条件满足即可。 + +**语法**: +```javascript +[condition1, 'or', condition2] +``` + +**示例**: +```javascript +// 状态是 active 或 pending +filters: [ + ['status', '=', 'active'], + 'or', + ['status', '=', 'pending'] +] + +// 多个 OR 条件 +filters: [ + ['priority', '=', 'high'], + 'or', + ['priority', '=', 'urgent'], + 'or', + ['priority', '=', 'critical'] +] + +// 等同于使用 IN +filters: [['priority', 'in', ['high', 'urgent', 'critical']]] +``` + +## 复杂条件示例 + +### 示例 1: 混合 AND/OR + +```javascript +// (status = 'active' OR status = 'pending') AND amount > 1000 +filters: [ + [ + ['status', '=', 'active'], + 'or', + ['status', '=', 'pending'] + ], + 'and', + ['amount', '>', 1000] +] +``` + +### 示例 2: 多层嵌套 + +```javascript +// status = 'active' AND (priority = 'high' OR (amount > 10000 AND customer_type = 'vip')) +filters: [ + ['status', '=', 'active'], + 'and', + [ + ['priority', '=', 'high'], + 'or', + [ + ['amount', '>', 10000], + 'and', + ['customer_type', '=', 'vip'] + ] + ] +] +``` + +### 示例 3: 日期范围和状态组合 + +```javascript +// 本月创建且状态为 active 或 pending 的记录 +filters: [ + ['created', '=', 'this_month'], + 'and', + [ + ['status', '=', 'active'], + 'or', + ['status', '=', 'pending'] + ] +] +``` + +### 示例 4: 数值范围和字符串匹配 + +```javascript +// 金额在 1000-5000 之间且名称包含"公司"的记录 +filters: [ + ['amount', 'between', [1000, 5000]], + 'and', + ['name', 'contains', '公司'] +] +``` + +## 操作符兼容性 + +| 操作符 | MongoDB | PostgreSQL | MySQL | 说明 | +|--------|---------|-----------|-------|------| +| = | ✓ | ✓ | ✓ | | +| != | ✓ | ✓ | ✓ | | +| > | ✓ | ✓ | ✓ | | +| >= | ✓ | ✓ | ✓ | | +| < | ✓ | ✓ | ✓ | | +| <= | ✓ | ✓ | ✓ | | +| contains | ✓ | ✓ | ✓ | | +| notcontains | ✓ | ✓ | ✓ | | +| startswith | ✓ | ✓ | ✓ | | +| endswith | ✓ | ✓ | ✓ | 部分数据库性能较低 | +| between | ✓ | ✓ | ✓ | | +| in | ✓ | ✓ | ✓ | | +| not in | ✓ | ✓ | ✓ | | + +## 性能提示 + +### 1. 使用索引字段 + +```javascript +// 推荐:在索引字段上过滤 +filters: [['status', '=', 'active']] + +// 注意:未索引字段可能较慢 +filters: [['description', 'contains', '关键词']] +``` + +### 2. 避免复杂的字符串操作 + +```javascript +// 推荐:精确匹配 +filters: [['status', '=', 'active']] + +// 注意:contains 查询较慢 +filters: [['name', 'contains', '关键词']] + +// 更慢:endswith 查询 +filters: [['name', 'endswith', '有限公司']] +``` + +### 3. 使用 IN 代替多个 OR + +```javascript +// 推荐:使用 IN +filters: [['status', 'in', ['active', 'pending', 'approved']]] + +// 不推荐:多个 OR +filters: [ + ['status', '=', 'active'], + 'or', + ['status', '=', 'pending'], + 'or', + ['status', '=', 'approved'] +] +``` + +### 4. 合理使用 between + +```javascript +// 推荐:使用 between +filters: [['amount', 'between', [1000, 5000]]] + +// 不推荐:分开的条件 +filters: [ + ['amount', '>=', 1000], + 'and', + ['amount', '<=', 5000] +] +``` + +## 常见错误 + +### 错误 1: 操作符拼写错误 + +```javascript +// 错误 +filters: [['status', '==', 'active']] // 应该是 '=' + +// 正确 +filters: [['status', '=', 'active']] +``` + +### 错误 2: 数组格式错误 + +```javascript +// 错误 +filters: ['status', '=', 'active'] // 缺少外层数组 + +// 正确 +filters: [['status', '=', 'active']] +``` + +### 错误 3: 嵌套条件格式错误 + +```javascript +// 错误 +filters: [ + 'status', '=', 'active', + 'and', + 'amount', '>', 1000 +] + +// 正确 +filters: [ + ['status', '=', 'active'], + 'and', + ['amount', '>', 1000] +] +``` + +## 相关文档 + +- [ObjectQL 概述](./README.md) +- [查询语法详解](./query-syntax.md) +- [ObjectQL 最佳实践](./best-practices.md) +- [对象元数据](../metadata/object-metadata.md) diff --git a/docs/objectql/query-syntax.md b/docs/objectql/query-syntax.md new file mode 100644 index 0000000000..9ad6385667 --- /dev/null +++ b/docs/objectql/query-syntax.md @@ -0,0 +1,524 @@ +# ObjectQL 查询语法详解 + +## 概述 + +ObjectQL 查询语法提供了一套统一的 API 来操作不同类型的数据库(MongoDB、PostgreSQL、MySQL 等),使开发者无需关心底层数据库的差异。 + +## 查询参数结构 + +### 标准查询参数 + +```javascript +{ + fields: [], // 要返回的字段列表 + filters: [], // 过滤条件 + sort: '', // 排序规则 + top: 100, // 返回记录数 + skip: 0 // 跳过记录数 +} +``` + +## Fields(字段选择) + +### 选择特定字段 + +```javascript +// 只返回 name 和 email 字段 +const records = await objects.accounts.find({ + fields: ['name', 'email'] +}); +``` + +### 选择所有字段 + +```javascript +// 不指定 fields 或传入空数组返回所有字段 +const records = await objects.accounts.find({ + fields: [] +}); +``` + +### 选择关联对象字段 + +```javascript +// 选择关联对象的字段 +const records = await objects.contacts.find({ + fields: ['name', 'account.name', 'account.industry'] +}); +``` + +### 字段别名 + +```javascript +// 使用 $ 符号访问原始字段值 +const records = await objects.accounts.find({ + fields: ['name', '$owner.name'] +}); +``` + +## Filters(过滤条件) + +### 基本过滤语法 + +```javascript +// 格式:[field, operator, value] +filters: [['field_name', 'operator', value]] +``` + +### 简单条件示例 + +```javascript +// 等于 +filters: [['status', '=', 'active']] + +// 不等于 +filters: [['status', '!=', 'inactive']] + +// 大于 +filters: [['amount', '>', 1000]] + +// 大于等于 +filters: [['amount', '>=', 1000]] + +// 小于 +filters: [['amount', '<', 5000]] + +// 小于等于 +filters: [['amount', '<=', 5000]] + +// 包含 +filters: [['name', 'contains', '公司']] + +// 不包含 +filters: [['name', 'notcontains', '测试']] + +// 以...开始 +filters: [['name', 'startswith', 'A']] + +// 以...结束 +filters: [['name', 'endswith', '有限公司']] + +// 在范围内 +filters: [['age', 'between', [20, 30]]] +``` + +### 空值判断 + +```javascript +// 为空 +filters: [['description', '=', null]] + +// 不为空 +filters: [['description', '!=', null]] +``` + +### 数组值查询 + +```javascript +// 值在数组中(IN 查询) +filters: [['status', '=', ['active', 'pending']]] + +// 等同于 +filters: [['status', 'in', ['active', 'pending']]] + +// 值不在数组中(NOT IN 查询) +filters: [['status', '!=', ['inactive', 'deleted']]] + +// 等同于 +filters: [['status', 'not in', ['inactive', 'deleted']]] +``` + +### 组合条件 + +#### AND 条件 + +```javascript +// 默认使用 AND 连接 +filters: [ + ['status', '=', 'active'], + ['amount', '>', 1000] +] + +// 等同于显式指定 AND +filters: [ + ['status', '=', 'active'], + 'and', + ['amount', '>', 1000] +] +``` + +#### OR 条件 + +```javascript +// 使用 OR 连接条件 +filters: [ + ['status', '=', 'active'], + 'or', + ['status', '=', 'pending'] +] +``` + +#### 复杂组合 + +```javascript +// (status = 'active' OR status = 'pending') AND amount > 1000 +filters: [ + [ + ['status', '=', 'active'], + 'or', + ['status', '=', 'pending'] + ], + 'and', + ['amount', '>', 1000] +] +``` + +```javascript +// status = 'active' AND (amount > 1000 OR priority = 'high') +filters: [ + ['status', '=', 'active'], + 'and', + [ + ['amount', '>', 1000], + 'or', + ['priority', '=', 'high'] + ] +] +``` + +### 关联对象查询 + +```javascript +// 查询关联对象字段 +filters: [['account.industry', '=', 'IT']] + +// 多层关联 +filters: [['account.owner.name', '=', '张三']] +``` + +### 当前用户变量 + +```javascript +// 使用 {userId} 表示当前用户 +filters: [['owner', '=', '{userId}']] + +// 使用 {spaceId} 表示当前工作区 +filters: [['space', '=', '{spaceId}']] +``` + +### 日期查询 + +```javascript +// 特定日期 +filters: [['created', '=', '2024-01-01']] + +// 日期范围 +filters: [['created', 'between', ['2024-01-01', '2024-12-31']]] + +// 相对日期 +filters: [['created', '=', 'this_month']] // 本月 +filters: [['created', '=', 'last_month']] // 上月 +filters: [['created', '=', 'this_year']] // 今年 +filters: [['created', '=', 'last_year']] // 去年 +filters: [['created', '=', 'this_week']] // 本周 +filters: [['created', '=', 'last_week']] // 上周 +filters: [['created', '=', 'today']] // 今天 +filters: [['created', '=', 'yesterday']] // 昨天 +filters: [['created', '=', 'tomorrow']] // 明天 +filters: [['created', '=', 'next_7_days']] // 未来7天 +filters: [['created', '=', 'next_30_days']] // 未来30天 +filters: [['created', '=', 'next_60_days']] // 未来60天 +filters: [['created', '=', 'next_90_days']] // 未来90天 +filters: [['created', '=', 'last_7_days']] // 过去7天 +filters: [['created', '=', 'last_30_days']] // 过去30天 +filters: [['created', '=', 'last_60_days']] // 过去60天 +filters: [['created', '=', 'last_90_days']] // 过去90天 +``` + +## Sort(排序) + +### 单字段排序 + +```javascript +// 升序 +const records = await objects.accounts.find({ + sort: 'name' +}); + +// 降序 +const records = await objects.accounts.find({ + sort: 'name desc' +}); +``` + +### 多字段排序 + +```javascript +// 多字段排序,用逗号分隔 +const records = await objects.accounts.find({ + sort: 'priority desc, created desc' +}); +``` + +### 数组格式排序 + +```javascript +// 使用数组格式 +const records = await objects.accounts.find({ + sort: [['priority', 'desc'], ['created', 'desc']] +}); +``` + +## Top 和 Skip(分页) + +### 限制返回数量 + +```javascript +// 返回前 10 条记录 +const records = await objects.accounts.find({ + top: 10 +}); +``` + +### 分页查询 + +```javascript +// 第一页(0-9) +const page1 = await objects.accounts.find({ + top: 10, + skip: 0 +}); + +// 第二页(10-19) +const page2 = await objects.accounts.find({ + top: 10, + skip: 10 +}); + +// 第三页(20-29) +const page3 = await objects.accounts.find({ + top: 10, + skip: 20 +}); +``` + +### 分页函数示例 + +```javascript +async function getPage(objectName, pageNumber, pageSize) { + const skip = (pageNumber - 1) * pageSize; + return await objects[objectName].find({ + top: pageSize, + skip: skip, + sort: 'created desc' + }); +} + +// 获取第 1 页,每页 20 条 +const page1 = await getPage('accounts', 1, 20); + +// 获取第 2 页,每页 20 条 +const page2 = await getPage('accounts', 2, 20); +``` + +## 完整查询示例 + +### 示例 1:基础查询 + +```javascript +// 查询活跃客户的名称和电话 +const activeAccounts = await objects.accounts.find({ + fields: ['name', 'phone', 'email'], + filters: [['status', '=', 'active']], + sort: 'name', + top: 50 +}); +``` + +### 示例 2:复杂条件查询 + +```javascript +// 查询高优先级或金额大于10000的活跃订单 +const orders = await objects.sales_orders.find({ + fields: ['order_number', 'account', 'amount', 'priority', 'status'], + filters: [ + ['status', '=', 'active'], + 'and', + [ + ['priority', '=', 'high'], + 'or', + ['amount', '>', 10000] + ] + ], + sort: 'created desc', + top: 100 +}); +``` + +### 示例 3:关联查询 + +```javascript +// 查询联系人及其所属客户信息 +const contacts = await objects.contacts.find({ + fields: [ + 'name', + 'email', + 'phone', + 'account.name', + 'account.industry', + 'account.owner.name' + ], + filters: [ + ['account.status', '=', 'active'], + 'and', + ['account.industry', '=', 'IT'] + ], + sort: 'name' +}); +``` + +### 示例 4:日期范围查询 + +```javascript +// 查询本月创建的订单 +const thisMonthOrders = await objects.sales_orders.find({ + fields: ['order_number', 'account', 'amount', 'created'], + filters: [['created', '=', 'this_month']], + sort: 'created desc' +}); + +// 查询指定日期范围的订单 +const rangeOrders = await objects.sales_orders.find({ + fields: ['order_number', 'account', 'amount', 'created'], + filters: [['created', 'between', ['2024-01-01', '2024-12-31']]], + sort: 'created desc' +}); +``` + +### 示例 5:当前用户相关查询 + +```javascript +// 查询当前用户拥有的记录 +const myRecords = await objects.accounts.find({ + fields: ['name', 'status', 'created'], + filters: [['owner', '=', '{userId}']], + sort: 'modified desc' +}, userSession); +``` + +### 示例 6:分页查询 + +```javascript +// 实现完整分页 +async function getPaginatedResults(pageNumber = 1, pageSize = 20) { + const skip = (pageNumber - 1) * pageSize; + + // 获取总数 + const total = await objects.accounts.count({ + filters: [['status', '=', 'active']] + }); + + // 获取当前页数据 + const records = await objects.accounts.find({ + fields: ['name', 'phone', 'email', 'status'], + filters: [['status', '=', 'active']], + sort: 'name', + top: pageSize, + skip: skip + }); + + return { + records: records, + total: total, + pageNumber: pageNumber, + pageSize: pageSize, + totalPages: Math.ceil(total / pageSize) + }; +} + +// 使用 +const result = await getPaginatedResults(1, 20); +console.log(`共 ${result.total} 条记录,当前第 ${result.pageNumber}/${result.totalPages} 页`); +``` + +## 性能优化建议 + +### 1. 只查询需要的字段 + +```javascript +// 推荐:只选择需要的字段 +const records = await objects.accounts.find({ + fields: ['name', 'phone'] +}); + +// 不推荐:查询所有字段 +const records = await objects.accounts.find({}); +``` + +### 2. 使用索引字段作为过滤条件 + +```javascript +// 推荐:使用索引字段 +filters: [['status', '=', 'active']] + +// 注意:非索引字段查询可能较慢 +filters: [['description', 'contains', '关键词']] +``` + +### 3. 合理使用 top 限制 + +```javascript +// 推荐:限制返回数量 +const records = await objects.accounts.find({ + top: 100 +}); + +// 不推荐:不限制可能返回大量数据 +const records = await objects.accounts.find({}); +``` + +### 4. 避免深层关联查询 + +```javascript +// 推荐:一层关联 +fields: ['name', 'account.name'] + +// 注意:多层关联可能影响性能 +fields: ['name', 'account.owner.manager.name'] +``` + +## 常见问题 + +### Q: 如何查询空值? + +```javascript +filters: [['field_name', '=', null]] +``` + +### Q: 如何实现 LIKE 查询? + +```javascript +// 包含 +filters: [['name', 'contains', '关键词']] + +// 以...开始 +filters: [['name', 'startswith', '前缀']] +``` + +### Q: 如何查询数组字段? + +```javascript +// 数组包含某个值 +filters: [['tags', '=', 'important']] +``` + +### Q: 如何进行不区分大小写的查询? + +ObjectQL 查询默认根据数据库特性处理大小写。对于需要不区分大小写的查询,建议在存储时统一转换为小写。 + +## 相关文档 + +- [ObjectQL 概述](./README.md) +- [过滤器操作符参考](./filter-operators.md) +- [ObjectQL 最佳实践](./best-practices.md) +- [对象元数据](../metadata/object-metadata.md) diff --git a/docs/trigger.md b/docs/triggers/README.md similarity index 100% rename from docs/trigger.md rename to docs/triggers/README.md diff --git a/docs/triggers/trigger-context.md b/docs/triggers/trigger-context.md new file mode 100644 index 0000000000..e782648a00 --- /dev/null +++ b/docs/triggers/trigger-context.md @@ -0,0 +1,644 @@ +# 触发器上下文详细说明 + +## 概述 + +触发器函数在执行时可以访问丰富的上下文信息,包括记录数据、用户信息、对象实例等。理解这些上下文对象是编写高效触发器的关键。 + +## 上下文属性 (this) + +在触发器函数中,`this` 对象包含以下属性: + +### 基本属性 + +| 属性 | 类型 | 说明 | 可用事件 | +|------|------|------|----------| +| userId | String | 当前用户 ID | 所有事件 | +| spaceId | String | 当前工作区 ID | 所有事件 | +| objectName | String | 当前对象名称 | 所有事件 | +| id | String | 记录 ID | 除 beforeInsert 外的所有事件 | +| doc | Object | 当前记录内容 | Insert/Update 事件 | +| previousDoc | Object | 操作前的记录内容 | Update/Delete 的 after 事件 | + +### 事件标志 + +| 属性 | 类型 | 说明 | +|------|------|------| +| isInsert | Boolean | 是否为插入操作 | +| isUpdate | Boolean | 是否为更新操作 | +| isDelete | Boolean | 是否为删除操作 | +| isFind | Boolean | 是否为查询操作 | +| isBefore | Boolean | 是否为 before 事件 | +| isAfter | Boolean | 是否为 after 事件 | + +### 查询相关 + +| 属性 | 类型 | 说明 | 可用事件 | +|------|------|------|----------| +| query | Object | 查询参数 | beforeFind | +| data | Array | 查询结果 | afterFind | + +## Context 对象 (ctx) + +触发器函数接收一个 `ctx` 参数,包含: + +```javascript +module.exports = { + beforeInsert: async function(ctx) { + // ctx 对象包含以下属性 + } +}; +``` + +### ctx.params + +包含触发器参数: + +```javascript +{ + isInsert: true, + isBefore: true, + userId: 'user123', + spaceId: 'space456', + objectName: 'sales_orders', + doc: { /* 记录数据 */ } +} +``` + +### ctx.broker + +Moleculer broker 实例,用于调用其他服务: + +```javascript +{ + meta: {}, // 元数据 + call: Function, // 调用服务方法 + emit: Function, // 发出事件 + broadcast: Function,// 广播事件 + namespace: String, // 命名空间 + nodeID: String, // 节点 ID + instanceID: String, // 实例 ID + logger: Object, // 日志对象 + metadata: Object // 元数据 +} +``` + +### 辅助方法 + +| 方法 | 参数 | 返回值 | 说明 | +|------|------|--------|------| +| getObject | objectName | Object | 获取对象实例 | +| getUser | userId, spaceId | Object | 获取用户会话信息 | +| makeNewID | - | String | 生成唯一 ID | + +## 详细说明 + +### 1. userId 和 spaceId + +当前操作用户和工作区的标识。 + +**示例**: +```javascript +module.exports = { + beforeInsert: async function() { + const { userId, spaceId } = this; + + console.log(`用户 ${userId} 在工作区 ${spaceId} 中创建记录`); + + // 获取用户信息 + const user = await this.getUser(userId, spaceId); + console.log(`用户名: ${user.name}`); + } +}; +``` + +### 2. doc - 当前记录 + +在 Insert 和 Update 事件中可用,包含即将保存或正在保存的记录数据。 + +**beforeInsert/beforeUpdate**: 可以修改 doc 的值 +```javascript +module.exports = { + beforeInsert: async function() { + const { doc } = this; + + // 读取字段 + console.log(`订单金额: ${doc.amount}`); + + // 修改字段(会保存到数据库) + doc.discount_amount = doc.amount * 0.1; + doc.final_amount = doc.amount - doc.discount_amount; + } +}; +``` + +**afterInsert/afterUpdate**: 只读,不能修改 +```javascript +module.exports = { + afterInsert: async function() { + const { doc, id } = this; + + // 可以读取字段 + console.log(`已创建订单: ${id}, 金额: ${doc.amount}`); + + // 但修改不会生效 + doc.status = 'processed'; // ❌ 不会保存 + } +}; +``` + +### 3. previousDoc - 原记录 + +在 Update 和 Delete 的 after 事件中可用,包含操作前的记录数据。 + +**比较变更**: +```javascript +module.exports = { + afterUpdate: async function() { + const { doc, previousDoc } = this; + + // 检查状态是否变更 + if (doc.status !== previousDoc.status) { + console.log(`状态从 ${previousDoc.status} 变更为 ${doc.status}`); + } + + // 检查金额是否变更 + if (doc.amount !== previousDoc.amount) { + console.log(`金额从 ${previousDoc.amount} 变更为 ${doc.amount}`); + } + } +}; +``` + +**beforeUpdate 获取原记录**: +```javascript +module.exports = { + beforeUpdate: async function() { + const { doc, id } = this; + + // beforeUpdate 没有 previousDoc,需要手动查询 + const previousDoc = await this.getObject(this.objectName).findOne(id); + + if (doc.status && doc.status !== previousDoc.status) { + // 验证状态转换 + } + } +}; +``` + +### 4. id - 记录 ID + +记录的唯一标识符。 + +**注意**: beforeInsert 事件中没有 id(因为还未保存到数据库) + +```javascript +module.exports = { + beforeInsert: async function() { + // ❌ beforeInsert 中 this.id 为 undefined + console.log(this.id); // undefined + + // 如果需要,可以生成一个 + const newId = this.makeNewID(); + this.doc._id = newId; + }, + + afterInsert: async function() { + // ✅ afterInsert 中 this.id 可用 + console.log(`已创建记录: ${this.id}`); + } +}; +``` + +### 5. query - 查询参数 + +在 beforeFind 事件中可用,包含查询参数,可以修改。 + +```javascript +module.exports = { + beforeFind: async function() { + const { query, userId } = this; + + console.log('原始查询:', query); + + // 修改查询条件 + if (!query.filters) { + query.filters = []; + } + + // 添加额外的过滤条件 + query.filters.push(['owner', '=', userId]); + + console.log('修改后查询:', query); + } +}; +``` + +### 6. data - 查询结果 + +在 afterFind 事件中可用,包含查询结果,可以修改。 + +```javascript +module.exports = { + afterFind: async function() { + const { data } = this; + + console.log(`查询返回 ${data.length} 条记录`); + + // 可以修改结果 + data.forEach(record => { + // 添加计算字段 + record.computed_field = record.field1 + record.field2; + + // 过滤敏感字段 + delete record.sensitive_field; + }); + } +}; +``` + +## 辅助方法 + +### getObject() + +获取对象实例,用于执行 CRUD 操作。 + +**语法**: +```javascript +const object = this.getObject(objectName) +``` + +**示例**: +```javascript +module.exports = { + afterInsert: async function() { + const { doc, id } = this; + + // 获取 contacts 对象 + const contactsObject = this.getObject('contacts'); + + // 创建联系人 + await contactsObject.insert({ + name: `${doc.name} - 主联系人`, + account: id + }); + + // 查询联系人 + const contacts = await contactsObject.find({ + filters: [['account', '=', id]] + }); + } +}; +``` + +### getUser() + +获取用户会话信息。 + +**语法**: +```javascript +const userSession = await this.getUser(userId, spaceId) +``` + +**返回值**: +```javascript +{ + userId: 'user123', + spaceId: 'space456', + name: '张三', + email: 'zhangsan@example.com', + roles: ['user', 'sales'], + permissions: { /* ... */ } +} +``` + +**示例**: +```javascript +module.exports = { + beforeInsert: async function() { + const { userId, spaceId } = this; + + // 获取用户信息 + const user = await this.getUser(userId, spaceId); + + // 验证用户角色 + if (!user.roles.includes('sales_manager')) { + throw new Error('只有销售经理可以创建此类型记录'); + } + + // 使用用户信息 + this.doc.created_by_name = user.name; + } +}; +``` + +### makeNewID() + +生成一个新的唯一 ID。 + +**语法**: +```javascript +const newId = this.makeNewID() +``` + +**示例**: +```javascript +module.exports = { + beforeInsert: async function() { + const { doc } = this; + + // 生成自定义 ID + if (!doc._id) { + doc._id = this.makeNewID(); + } + + // 为子记录生成 ID + if (doc.items && doc.items.length > 0) { + doc.items.forEach(item => { + if (!item._id) { + item._id = this.makeNewID(); + } + }); + } + } +}; +``` + +## 全局对象 + +触发器中可以访问一些全局对象: + +### _ (Lodash) + +```javascript +module.exports = { + beforeInsert: async function() { + const { doc } = this; + + // 使用 Lodash + const uniqueTags = _.uniq(doc.tags); + const sortedItems = _.sortBy(doc.items, 'price'); + } +}; +``` + +### moment + +```javascript +module.exports = { + beforeInsert: async function() { + const { doc } = this; + + // 使用 Moment.js + doc.due_date = moment().add(30, 'days').toDate(); + doc.formatted_date = moment(doc.date).format('YYYY-MM-DD'); + } +}; +``` + +### validator + +```javascript +module.exports = { + beforeInsert: async function() { + const { doc } = this; + + // 使用 Validator + if (!validator.isEmail(doc.email)) { + throw new Error('无效的邮箱地址'); + } + + if (!validator.isURL(doc.website)) { + throw new Error('无效的网址'); + } + } +}; +``` + +### Filters + +```javascript +module.exports = { + beforeFind: async function() { + const { query } = this; + + // 使用 Filters 工具 + const Filters = require('@steedos/filters'); + + // 格式化过滤条件 + query.filters = Filters.formatFilters(query.filters); + } +}; +``` + +## Broker 调用 + +使用 `ctx.broker` 或 `this.broker` 调用其他微服务: + +### 调用服务方法 + +```javascript +module.exports = { + afterInsert: async function() { + const { doc, id } = this; + + // 调用通知服务 + await this.broker.call('notifications.send', { + to: doc.owner, + title: '新记录创建', + message: `记录 ${doc.name} 已创建`, + link: `/app/${this.objectName}/${id}` + }); + + // 调用邮件服务 + await this.broker.call('email.send', { + to: doc.email, + subject: '欢迎', + template: 'welcome', + data: { name: doc.name } + }); + } +}; +``` + +### 发出事件 + +```javascript +module.exports = { + afterUpdate: async function() { + const { doc, previousDoc, id } = this; + + // 发出自定义事件 + if (doc.status === 'completed' && previousDoc.status !== 'completed') { + this.broker.emit('order.completed', { + orderId: id, + orderNumber: doc.order_number, + amount: doc.amount + }); + } + } +}; +``` + +## 完整示例 + +```javascript +/** + * 销售订单触发器 + * + * 功能: + * 1. 自动生成订单号 + * 2. 验证订单金额 + * 3. 状态变更通知 + * 4. 更新客户统计 + */ +module.exports = { + listenTo: 'sales_orders', + + // 插入前:生成订单号、验证数据 + beforeInsert: async function() { + const { doc, userId, spaceId } = this; + + // 验证金额 + if (!doc.amount || doc.amount <= 0) { + throw new Error('订单金额必须大于0'); + } + + // 获取用户信息 + const user = await this.getUser(userId, spaceId); + + // 生成订单号 + if (!doc.order_number) { + const dateStr = moment().format('YYYYMMDD'); + const sequence = await this.getNextSequence('sales_orders', dateStr); + doc.order_number = `SO-${dateStr}-${sequence.toString().padStart(4, '0')}`; + } + + // 记录创建人信息 + doc.created_by_name = user.name; + }, + + // 插入后:创建关联记录、发送通知 + afterInsert: async function() { + const { doc, id } = this; + + // 创建订单明细(如果有) + if (doc.items && doc.items.length > 0) { + const orderItemsObject = this.getObject('order_items'); + for (const item of doc.items) { + await orderItemsObject.insert({ + ...item, + order: id + }); + } + } + + // 发送通知 + await this.broker.call('notifications.send', { + to: doc.owner, + title: '新订单创建', + message: `订单 ${doc.order_number} 已创建`, + link: `/app/sales_orders/${id}` + }); + }, + + // 更新前:验证状态转换 + beforeUpdate: async function() { + const { doc, id } = this; + + // 如果状态变更,验证转换 + if (doc.status) { + const previousDoc = await this.getObject('sales_orders').findOne(id); + + if (previousDoc.status !== doc.status) { + this.validateStatusChange(previousDoc.status, doc.status); + } + } + }, + + // 更新后:状态变更处理、更新统计 + afterUpdate: async function() { + const { doc, previousDoc, id } = this; + + // 状态变更通知 + if (doc.status && doc.status !== previousDoc.status) { + await this.broker.call('notifications.send', { + to: doc.owner, + title: '订单状态变更', + message: `订单 ${doc.order_number} 状态变更为 ${doc.status}`, + link: `/app/sales_orders/${id}` + }); + } + + // 金额变更时更新客户统计 + if (doc.amount !== previousDoc.amount) { + await this.updateAccountTotals(doc.account); + } + }, + + // 删除前:检查依赖 + beforeDelete: async function() { + const { id } = this; + + // 检查是否有关联的发票 + const invoiceCount = await this.getObject('invoices').count({ + filters: [['order', '=', id]] + }); + + if (invoiceCount > 0) { + throw new Error('此订单已有发票,无法删除'); + } + }, + + // 删除后:清理数据、更新统计 + afterDelete: async function() { + const { previousDoc, id } = this; + + // 删除订单明细 + const orderItems = await this.getObject('order_items').find({ + filters: [['order', '=', id]] + }); + + for (const item of orderItems) { + await this.getObject('order_items').delete(item._id); + } + + // 更新客户统计 + await this.updateAccountTotals(previousDoc.account); + }, + + // 辅助方法 + validateStatusChange: function(oldStatus, newStatus) { + const validTransitions = { + 'draft': ['submitted', 'cancelled'], + 'submitted': ['approved', 'rejected'], + 'approved': ['completed', 'cancelled'], + 'rejected': [], + 'completed': [], + 'cancelled': [] + }; + + const allowed = validTransitions[oldStatus] || []; + if (!allowed.includes(newStatus)) { + throw new Error(`不能从 ${oldStatus} 变更为 ${newStatus}`); + } + }, + + getNextSequence: async function(prefix, date) { + // 获取序列号的实现 + // ... + return 1; + }, + + updateAccountTotals: async function(accountId) { + // 更新客户统计的实现 + // ... + } +}; +``` + +## 相关文档 + +- [触发器概述](./README.md) +- [触发器类型和使用场景](./trigger-types.md) +- [ObjectQL 查询](../objectql/) +- [对象元数据](../metadata/object-metadata.md) diff --git a/docs/triggers/trigger-types.md b/docs/triggers/trigger-types.md new file mode 100644 index 0000000000..bfb9313de2 --- /dev/null +++ b/docs/triggers/trigger-types.md @@ -0,0 +1,604 @@ +# 触发器类型和使用场景 + +## 概述 + +触发器 (Trigger) 是在数据操作前后自动执行的服务端代码,用于实现业务逻辑、数据验证、自动化流程等功能。 + +## 触发器类型 + +### Before 触发器(操作前) + +在数据库操作之前执行,可以: +- 验证数据 +- 修改即将保存的数据 +- 阻止不合法的操作 + +### After 触发器(操作后) + +在数据库操作之后执行,可以: +- 执行关联操作 +- 发送通知 +- 同步数据到其他系统 + +## 触发器事件 + +| 事件 | 触发时机 | 主要用途 | +|------|----------|----------| +| beforeInsert | 插入记录前 | 数据验证、自动填充字段 | +| afterInsert | 插入记录后 | 创建关联记录、发送通知 | +| beforeUpdate | 更新记录前 | 验证变更、记录审计 | +| afterUpdate | 更新记录后 | 同步数据、触发工作流 | +| beforeDelete | 删除记录前 | 检查依赖、阻止删除 | +| afterDelete | 删除记录后 | 清理关联数据 | +| beforeFind | 查询记录前 | 修改查询条件 | +| afterFind | 查询记录后 | 过滤结果、补充数据 | + +## 使用场景 + +### 1. 数据验证 + +#### 场景:验证订单金额必须大于0 + +```javascript +module.exports = { + listenTo: 'sales_orders', + + beforeInsert: async function() { + const { doc } = this; + + if (doc.amount <= 0) { + throw new Error('订单金额必须大于0'); + } + }, + + beforeUpdate: async function() { + const { doc } = this; + + if (doc.amount !== undefined && doc.amount <= 0) { + throw new Error('订单金额必须大于0'); + } + } +}; +``` + +#### 场景:验证日期逻辑 + +```javascript +module.exports = { + listenTo: 'projects', + + beforeInsert: async function() { + const { doc } = this; + + if (doc.end_date < doc.start_date) { + throw new Error('结束日期不能早于开始日期'); + } + }, + + beforeUpdate: async function() { + const { doc, previousDoc } = this; + + const startDate = doc.start_date || previousDoc.start_date; + const endDate = doc.end_date || previousDoc.end_date; + + if (endDate < startDate) { + throw new Error('结束日期不能早于开始日期'); + } + } +}; +``` + +### 2. 自动填充字段 + +#### 场景:自动生成编号 + +```javascript +module.exports = { + listenTo: 'sales_orders', + + beforeInsert: async function() { + const { doc } = this; + + if (!doc.order_number) { + // 生成订单号:SO-20240101-0001 + const date = new Date(); + const dateStr = date.toISOString().slice(0, 10).replace(/-/g, ''); + + // 查询当天最大编号 + const lastOrder = await this.getObject('sales_orders').find({ + filters: [['order_number', 'startswith', `SO-${dateStr}`]], + sort: 'order_number desc', + top: 1 + }); + + let sequence = 1; + if (lastOrder.length > 0) { + const lastNumber = lastOrder[0].order_number.split('-')[2]; + sequence = parseInt(lastNumber) + 1; + } + + doc.order_number = `SO-${dateStr}-${sequence.toString().padStart(4, '0')}`; + } + } +}; +``` + +#### 场景:计算字段值 + +```javascript +module.exports = { + listenTo: 'sales_orders', + + beforeInsert: async function() { + const { doc } = this; + + // 计算总金额 = 小计 + 税额 + doc.total_amount = (doc.subtotal || 0) + (doc.tax || 0); + }, + + beforeUpdate: async function() { + const { doc, previousDoc } = this; + + // 如果小计或税额变更,重新计算总金额 + if (doc.subtotal !== undefined || doc.tax !== undefined) { + const subtotal = doc.subtotal !== undefined ? doc.subtotal : previousDoc.subtotal; + const tax = doc.tax !== undefined ? doc.tax : previousDoc.tax; + doc.total_amount = subtotal + tax; + } + } +}; +``` + +### 3. 状态转换控制 + +#### 场景:限制状态变更路径 + +```javascript +module.exports = { + listenTo: 'sales_orders', + + beforeUpdate: async function() { + const { doc, previousDoc } = this; + + // 只有状态变更时才验证 + if (doc.status && doc.status !== previousDoc.status) { + const validTransitions = { + 'draft': ['submitted', 'cancelled'], + 'submitted': ['approved', 'rejected'], + 'approved': ['completed', 'cancelled'], + 'rejected': [], + 'completed': [], + 'cancelled': [] + }; + + const allowedNext = validTransitions[previousDoc.status] || []; + + if (!allowedNext.includes(doc.status)) { + throw new Error( + `不能从 ${previousDoc.status} 变更为 ${doc.status}` + ); + } + } + } +}; +``` + +### 4. 权限控制 + +#### 场景:只有创建人可以删除 + +```javascript +module.exports = { + listenTo: 'documents', + + beforeDelete: async function() { + const { previousDoc, userId } = this; + + if (previousDoc.created_by !== userId) { + throw new Error('只有创建人可以删除此文档'); + } + } +}; +``` + +#### 场景:特定角色才能修改关键字段 + +```javascript +module.exports = { + listenTo: 'sales_orders', + + beforeUpdate: async function() { + const { doc, previousDoc, userId, spaceId } = this; + + // 如果折扣变更,检查权限 + if (doc.discount !== undefined && doc.discount !== previousDoc.discount) { + const user = await this.getUser(userId, spaceId); + + if (!user.roles.includes('sales_manager')) { + throw new Error('只有销售经理可以修改折扣'); + } + } + } +}; +``` + +### 5. 创建关联记录 + +#### 场景:创建客户时自动创建默认联系人 + +```javascript +module.exports = { + listenTo: 'accounts', + + afterInsert: async function() { + const { doc, id } = this; + + // 创建默认联系人 + await this.getObject('contacts').insert({ + name: `${doc.name} - 主要联系人`, + account: id, + is_primary: true + }); + } +}; +``` + +#### 场景:订单完成后创建发票 + +```javascript +module.exports = { + listenTo: 'sales_orders', + + afterUpdate: async function() { + const { doc, previousDoc, id } = this; + + // 订单状态变为"已完成"时创建发票 + if (doc.status === 'completed' && previousDoc.status !== 'completed') { + await this.getObject('invoices').insert({ + order: id, + order_number: doc.order_number, + amount: doc.total_amount, + due_date: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000), // 30天后 + status: 'pending' + }); + } + } +}; +``` + +### 6. 同步数据 + +#### 场景:客户信息变更时同步到联系人 + +```javascript +module.exports = { + listenTo: 'accounts', + + afterUpdate: async function() { + const { doc, previousDoc, id } = this; + + // 如果行业变更,更新所有关联联系人的行业字段 + if (doc.industry && doc.industry !== previousDoc.industry) { + const contacts = await this.getObject('contacts').find({ + filters: [['account', '=', id]] + }); + + for (const contact of contacts) { + await this.getObject('contacts').update(contact._id, { + account_industry: doc.industry + }); + } + } + } +}; +``` + +### 7. 发送通知 + +#### 场景:任务分配时通知负责人 + +```javascript +module.exports = { + listenTo: 'tasks', + + afterInsert: async function() { + const { doc, id } = this; + + // 发送通知给负责人 + await this.broker.call('notifications.send', { + to: doc.assigned_to, + title: '新任务分配', + message: `您有一个新任务:${doc.name}`, + link: `/app/tasks/${id}` + }); + }, + + afterUpdate: async function() { + const { doc, previousDoc, id } = this; + + // 负责人变更时通知新负责人 + if (doc.assigned_to && doc.assigned_to !== previousDoc.assigned_to) { + await this.broker.call('notifications.send', { + to: doc.assigned_to, + title: '任务转交', + message: `任务"${doc.name}"已转交给您`, + link: `/app/tasks/${id}` + }); + } + } +}; +``` + +### 8. 审计日志 + +#### 场景:记录重要字段的变更历史 + +```javascript +module.exports = { + listenTo: 'contracts', + + afterUpdate: async function() { + const { doc, previousDoc, id, userId } = this; + + // 监控的重要字段 + const auditFields = ['amount', 'status', 'expiry_date']; + + for (const field of auditFields) { + if (doc[field] !== undefined && doc[field] !== previousDoc[field]) { + // 记录变更 + await this.getObject('audit_logs').insert({ + object_name: 'contracts', + record_id: id, + field_name: field, + old_value: previousDoc[field], + new_value: doc[field], + changed_by: userId, + changed_at: new Date() + }); + } + } + } +}; +``` + +### 9. 检查依赖关系 + +#### 场景:删除前检查是否有关联记录 + +```javascript +module.exports = { + listenTo: 'accounts', + + beforeDelete: async function() { + const { id } = this; + + // 检查是否有关联的联系人 + const contactCount = await this.getObject('contacts').count({ + filters: [['account', '=', id]] + }); + + if (contactCount > 0) { + throw new Error(`此客户有 ${contactCount} 个关联联系人,无法删除`); + } + + // 检查是否有关联的订单 + const orderCount = await this.getObject('sales_orders').count({ + filters: [['account', '=', id]] + }); + + if (orderCount > 0) { + throw new Error(`此客户有 ${orderCount} 个关联订单,无法删除`); + } + } +}; +``` + +### 10. 数据清理 + +#### 场景:删除记录后清理关联数据 + +```javascript +module.exports = { + listenTo: 'accounts', + + afterDelete: async function() { + const { id } = this; + + // 删除关联的联系人(如果配置为级联删除) + const contacts = await this.getObject('contacts').find({ + filters: [['account', '=', id]] + }); + + for (const contact of contacts) { + await this.getObject('contacts').delete(contact._id); + } + + // 删除关联的附件 + await this.broker.call('files.deleteByRecord', { + recordId: id + }); + } +}; +``` + +### 11. 汇总计算 + +#### 场景:订单变更时更新客户的订单总额 + +```javascript +module.exports = { + listenTo: 'sales_orders', + + afterInsert: async function() { + await this.updateAccountTotals(this.doc.account); + }, + + afterUpdate: async function() { + const { doc, previousDoc } = this; + + // 如果金额或客户变更,更新相关客户的总额 + if (doc.amount !== previousDoc.amount || doc.account !== previousDoc.account) { + await this.updateAccountTotals(doc.account); + + if (doc.account !== previousDoc.account) { + await this.updateAccountTotals(previousDoc.account); + } + } + }, + + afterDelete: async function() { + await this.updateAccountTotals(this.previousDoc.account); + }, + + // 辅助方法 + updateAccountTotals: async function(accountId) { + if (!accountId) return; + + // 计算该客户的订单总额 + const result = await this.getObject('sales_orders').aggregate({ + filters: [ + ['account', '=', accountId], + ['status', '!=', 'cancelled'] + ] + }, [ + { $group: { _id: null, total: { $sum: '$amount' } } } + ]); + + const totalAmount = result[0]?.total || 0; + + // 更新客户记录 + await this.getObject('accounts').update(accountId, { + total_order_amount: totalAmount + }); + } +}; +``` + +### 12. 数据同步到外部系统 + +#### 场景:订单创建后同步到 ERP 系统 + +```javascript +module.exports = { + listenTo: 'sales_orders', + + afterInsert: async function() { + const { doc, id } = this; + + try { + // 调用 ERP API + const response = await this.broker.call('erp.createOrder', { + order_number: doc.order_number, + customer_code: doc.customer_code, + amount: doc.total_amount, + items: doc.items + }); + + // 保存 ERP 系统返回的 ID + await this.getObject('sales_orders').update(id, { + erp_id: response.id, + sync_status: 'synced', + synced_at: new Date() + }); + } catch (error) { + // 记录同步失败 + await this.getObject('sales_orders').update(id, { + sync_status: 'failed', + sync_error: error.message + }); + } + } +}; +``` + +## 最佳实践 + +### 1. 保持触发器简单 + +- 触发器应该只包含核心逻辑 +- 复杂操作应该封装到服务中 +- 避免在触发器中进行大量计算 + +### 2. 避免无限循环 + +```javascript +// ❌ 错误:可能导致无限循环 +module.exports = { + afterUpdate: async function() { + const { id } = this; + await this.getObject('sales_orders').update(id, { + modified: new Date() // 这会再次触发 afterUpdate + }); + } +}; + +// ✅ 正确:使用标志避免循环 +module.exports = { + afterUpdate: async function() { + const { id, doc } = this; + + if (!doc._skipTrigger) { + await this.getObject('sales_orders').update(id, { + modified: new Date(), + _skipTrigger: true + }); + } + } +}; +``` + +### 3. 处理错误 + +```javascript +module.exports = { + afterInsert: async function() { + const { doc, id } = this; + + try { + // 可能失败的操作 + await sendNotification(doc.owner); + } catch (error) { + // 记录错误但不阻断流程 + console.error('发送通知失败:', error); + } + } +}; +``` + +### 4. 使用事务保证一致性 + +对于需要保证数据一致性的操作,应该使用事务。 + +### 5. 文档化触发器逻辑 + +```javascript +/** + * 订单触发器 + * + * beforeInsert: + * - 验证订单金额 + * - 生成订单号 + * + * afterInsert: + * - 创建发票 + * - 发送通知 + * + * beforeUpdate: + * - 验证状态转换 + * + * afterUpdate: + * - 更新客户总额 + * - 同步到 ERP + */ +module.exports = { + listenTo: 'sales_orders', + // ... +}; +``` + +## 相关文档 + +- [触发器概述](./README.md) +- [触发器上下文](./trigger-context.md) +- [ObjectQL 查询](../objectql/) +- [对象元数据](../metadata/object-metadata.md) From a530c906d895a095dce117a4beeae34ba8790823 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 14 Jan 2026 05:40:53 +0000 Subject: [PATCH 3/4] Add comprehensive documentation for AI/Copilot recognition Co-authored-by: baozhoutao <6194462+baozhoutao@users.noreply.github.com> --- .github/copilot-instructions.md | 360 ++++++++++++++++++++++ DOCUMENTATION_INDEX.md | 272 +++++++++++++++++ QUICK_REFERENCE_CARD.md | 422 ++++++++++++++++++++++++++ REPOSITORY_SUMMARY.md | 522 ++++++++++++++++++++++++++++++++ 4 files changed, 1576 insertions(+) create mode 100644 .github/copilot-instructions.md create mode 100644 DOCUMENTATION_INDEX.md create mode 100644 QUICK_REFERENCE_CARD.md create mode 100644 REPOSITORY_SUMMARY.md diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 0000000000..2c4146f87f --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,360 @@ +# Steedos Platform - GitHub Copilot Instructions + +## Project Overview + +Steedos Platform is an **AI-Native Low-Code Platform** that combines metadata-driven architecture with generative AI to enable rapid enterprise application development. The platform supports "Prompt to App" development, allowing developers to create data models, UIs, and microservices using natural language. + +## Tech Stack + +- **Backend**: Node.js, Moleculer (microservices), MongoDB/PostgreSQL/MySQL +- **Frontend**: React, Baidu Amis (Low-Code UI Framework) +- **Architecture**: Metadata-Driven, Event-Driven Microservices +- **Query Language**: ObjectQL (cross-database abstraction layer) + +## Key Concepts + +### 1. Metadata-Driven Architecture + +Everything in Steedos is defined through metadata: + +```yaml +# Example: Object Definition +name: accounts +label: Account +fields: + name: + type: text + label: Name + required: true + annual_revenue: + type: currency + label: Annual Revenue +``` + +### 2. ObjectQL - Query Abstraction Layer + +ObjectQL provides a unified query interface across different databases: + +```javascript +// Query with permissions +const accounts = await objects.accounts.find({ + fields: ['name', 'annual_revenue'], + filters: [['status', '=', 'active']], + sort: 'name', + top: 100 +}, userSession); +``` + +### 3. Triggers - Business Logic + +Triggers execute before/after data operations: + +```javascript +module.exports = { + listenTo: 'accounts', + beforeInsert: async function() { + const { doc } = this; + // Validate and modify data before saving + } +}; +``` + +## Project Structure + +``` +steedos-platform/ +├── packages/ # Core NPM packages (26 packages) +│ ├── objectql/ # Query abstraction layer +│ ├── metadata-core/ # Metadata engine +│ ├── accounts/ # User authentication +│ └── ... +├── services/ # Microservices (39 services) +│ ├── service-metadata-objects/ +│ ├── service-api/ +│ └── ... +├── builder6/ # Builder application +│ ├── server/ # Main server +│ └── webapp/ # React frontend +├── docs/ # Documentation +│ ├── metadata/ # Metadata docs +│ ├── objectql/ # ObjectQL docs +│ └── triggers/ # Trigger docs +└── test/ # Tests +``` + +## Development Guidelines + +### Metadata Development + +1. **Object Definition** - Define business entities in `.object.yml` files +2. **Field Types** - Use appropriate field types (text, number, lookup, master_detail, formula, etc.) +3. **Inheritance** - Extend standard objects by creating same-named objects in custom packages +4. **Permissions** - Define object and field-level permissions in `.permissionset.yml` files + +### ObjectQL Usage + +1. **Always pass userSession** - Ensure permission checks: `objects.xxx.find({...}, userSession)` +2. **Select specific fields** - Only query needed fields: `fields: ['name', 'status']` +3. **Use indexed fields** - Filter on indexed fields for better performance +4. **Limit results** - Always use `top` parameter to limit result size + +### Trigger Development + +1. **Keep triggers simple** - Complex logic should be in services +2. **Avoid infinite loops** - Be careful with update operations in afterUpdate +3. **Handle errors** - Always wrap external calls in try-catch +4. **Document purpose** - Add comments explaining trigger logic + +### Coding Standards + +- **Language**: TypeScript preferred, JavaScript acceptable +- **Style**: Follow ESLint configuration +- **Naming**: + - Objects: `snake_case` (e.g., `sales_orders`) + - Fields: `snake_case` (e.g., `order_number`) + - Services: `kebab-case` (e.g., `service-metadata-objects`) + - Classes: `PascalCase` + - Functions: `camelCase` + +## Common Patterns + +### Creating a New Object + +```yaml +# objects/custom_object.object.yml +name: custom_object +label: Custom Object +icon: custom1 +fields: + name: + type: text + label: Name + required: true + status: + type: select + label: Status + options: + - label: Active + value: active + - label: Inactive + value: inactive +``` + +### Querying Related Objects + +```javascript +// Query with relationship traversal +const contacts = await objects.contacts.find({ + fields: ['name', 'account.name', 'account.industry'], + filters: [ + ['account.status', '=', 'active'], + 'and', + ['is_primary', '=', true] + ] +}, userSession); +``` + +### Implementing Validation + +```javascript +// triggers/custom_object.trigger.js +module.exports = { + listenTo: 'custom_object', + + beforeInsert: async function() { + const { doc } = this; + + if (doc.amount < 0) { + throw new Error('Amount must be greater than 0'); + } + } +}; +``` + +### Creating Related Records + +```javascript +// triggers/accounts.trigger.js +module.exports = { + listenTo: 'accounts', + + afterInsert: async function() { + const { doc, id } = this; + + // Create default contact + await this.getObject('contacts').insert({ + name: `${doc.name} - Primary Contact`, + account: id, + is_primary: true + }); + } +}; +``` + +## Metadata Inheritance Rules + +When multiple packages define the same object: + +1. **Objects merge** - Fields from all packages are combined +2. **Fields override** - Later packages override field properties +3. **Arrays append** - Array properties (like options) are appended +4. **Objects deep merge** - Object properties merge recursively +5. **Triggers replace** - Later triggers completely replace earlier ones + +Example: + +```yaml +# Base package +name: accounts +fields: + phone: + type: text + required: false + +# Extension package (loaded later) +name: accounts +fields: + phone: + required: true # Override + industry: # Add new field + type: select +``` + +## Testing + +```bash +# Run tests for specific package +cd packages/objectql +yarn test + +# Run tests with coverage +yarn test:coverage + +# Run specific test file +yarn test src/test.spec.ts +``` + +## Documentation References + +- **[Metadata Documentation](../docs/metadata/)** - Complete metadata guide +- **[ObjectQL Guide](../docs/objectql/)** - Query language reference +- **[Trigger Guide](../docs/triggers/)** - Trigger development +- **[Developer Guide](../docs/DEVELOPER_GUIDE.md)** - Setup and workflows +- **[Architecture](../docs/CORE_ARCHITECTURE_EN.md)** - System architecture + +## Microservices Architecture + +Steedos uses Moleculer for microservices: + +```javascript +// Example service action +module.exports = { + name: "accounts", + + actions: { + create: { + async handler(ctx) { + const { doc } = ctx.params; + // Trigger beforeInsert + // Save to database + // Trigger afterInsert + return result; + } + } + }, + + events: { + "account.created": { + async handler(ctx) { + // React to account creation + } + } + } +}; +``` + +## Permission Model + +Permissions are evaluated in this order (most privileged wins): + +1. **Profile** - Base permissions +2. **Permission Sets** - Additional permissions (union) +3. **Object Settings** - Object-level defaults +4. **Sharing Rules** - Record-level sharing +5. **Manual Sharing** - Individual record sharing + +## AI Integration Points + +When writing code for AI features: + +1. **Metadata Generation** - Generate valid YAML metadata from natural language +2. **UI Generation** - Create Amis JSON configurations for pages +3. **Code Generation** - Generate Node.js code for business logic +4. **Schema Inference** - Infer object relationships from descriptions + +## Environment Variables + +Key environment variables: + +- `ROOT_URL` - Application root URL +- `MONGO_URL` - MongoDB connection string +- `MONGO_OPLOG_URL` - MongoDB oplog URL +- `STEEDOS_TENANT_ENABLE_REGISTER` - Enable tenant registration +- `STEEDOS_CFS_STORE` - File storage type (local/s3) + +## Build Commands + +```bash +# Install dependencies +yarn + +# Build all packages +yarn build + +# Start development server +yarn start + +# Start with watch mode +yarn dev + +# Build Docker image +docker build -t steedos/steedos-platform . +``` + +## Common Issues and Solutions + +### Issue: "Object not found" +- Ensure object metadata is loaded +- Check package dependencies +- Verify object name spelling + +### Issue: "Permission denied" +- Check userSession is passed +- Verify permission set configuration +- Review object-level permissions + +### Issue: "Trigger not firing" +- Check listenTo matches object name +- Verify trigger file location +- Ensure server restarted after changes + +## When to Use What + +- **Objects** - For data models and entities +- **Triggers** - For validation and automation +- **Actions** - For custom API endpoints +- **Buttons** - For custom UI actions +- **Workflows** - For approval processes +- **Reports** - For data analysis +- **Dashboards** - For data visualization + +## Code Examples Repository + +For more examples, see: +- [Steedos Templates](https://github.com/steedos/steedos-templates) +- [Example Apps](../test/) +- [Service Examples](../services/) + +--- + +**Remember**: Steedos is metadata-driven. Always think "configuration over code" first, then implement custom logic only when necessary. diff --git a/DOCUMENTATION_INDEX.md b/DOCUMENTATION_INDEX.md new file mode 100644 index 0000000000..f339ad0620 --- /dev/null +++ b/DOCUMENTATION_INDEX.md @@ -0,0 +1,272 @@ +# Steedos Platform - Complete Documentation Index + +> 📚 **Comprehensive guide to all documentation in this repository** +> Use this index to quickly find the information you need + +## 🎯 Getting Started + +| Document | Description | Audience | +|----------|-------------|----------| +| [README.md](README.md) | Project overview and quick start | Everyone | +| [README_cn.md](README_cn.md) | 项目概述和快速开始(中文) | Everyone | +| [REPOSITORY_SUMMARY.md](REPOSITORY_SUMMARY.md) | Complete repository summary | Developers | +| [QUICK_REFERENCE_CARD.md](QUICK_REFERENCE_CARD.md) | Quick reference for common tasks | Developers | +| [CONTRIBUTING.md](CONTRIBUTING.md) | Contribution guidelines | Contributors | + +## 📖 Core Documentation + +### Architecture & Design + +| Document | Description | +|----------|-------------| +| [docs/CORE_ARCHITECTURE_EN.md](docs/CORE_ARCHITECTURE_EN.md) | Complete system architecture guide | +| [docs/DEVELOPER_GUIDE.md](docs/DEVELOPER_GUIDE.md) | Developer setup and workflows | +| [docs/PACKAGES_INDEX.md](docs/PACKAGES_INDEX.md) | Index of all 26 packages and 39 services | +| [docs/QUICK_REFERENCE.md](docs/QUICK_REFERENCE.md) | Quick reference for APIs and configs | +| [docs/object-service.md](docs/object-service.md) | Object service architecture | +| [docs/env.md](docs/env.md) | Environment variables reference | + +### Metadata System + +| Document | Description | +|----------|-------------| +| [docs/metadata/README.md](docs/metadata/README.md) | Metadata system overview | +| [docs/metadata/metadata-types.md](docs/metadata/metadata-types.md) | Complete metadata types reference | +| [docs/metadata/inheritance-rules.md](docs/metadata/inheritance-rules.md) | Metadata inheritance and override rules | +| [docs/metadata/object-metadata.md](docs/metadata/object-metadata.md) | Object metadata detailed guide | +| [docs/metadata/field-types.md](docs/metadata/field-types.md) | All field types complete reference | +| [docs/metadata/permissions.md](docs/metadata/permissions.md) | Permission configuration guide | + +### ObjectQL Query Language + +| Document | Description | +|----------|-------------| +| [docs/objectql/README.md](docs/objectql/README.md) | ObjectQL overview and core methods | +| [docs/objectql/query-syntax.md](docs/objectql/query-syntax.md) | Complete query syntax guide | +| [docs/objectql/filter-operators.md](docs/objectql/filter-operators.md) | All filter operators reference | +| [docs/objectql/best-practices.md](docs/objectql/best-practices.md) | Best practices and optimization | + +### Triggers & Business Logic + +| Document | Description | +|----------|-------------| +| [docs/triggers/README.md](docs/triggers/README.md) | Trigger system overview | +| [docs/triggers/trigger-types.md](docs/triggers/trigger-types.md) | Trigger types and use cases (12 scenarios) | +| [docs/triggers/trigger-context.md](docs/triggers/trigger-context.md) | Trigger context and API reference | + +## 🔧 Technical Documentation + +### Package Documentation + +| Package | Documentation | +|---------|---------------| +| `@steedos/objectql` | [packages/objectql/README.md](packages/objectql/README.md) | +| `@steedos/metadata-core` | [packages/metadata-core/README.md](packages/metadata-core/README.md) | +| `@steedos/metadata-api` | [packages/metadata-api/README.md](packages/metadata-api/README.md) | +| `@steedos/metadata-registrar` | [packages/metadata-registrar/README.md](packages/metadata-registrar/README.md) | +| `@steedos/formula` | [packages/formula/README.md](packages/formula/README.md) | +| `@steedos/filters` | [packages/filters/README.md](packages/filters/README.md) | + +### Service Documentation + +| Service | Documentation | +|---------|---------------| +| `service-metadata` | [services/service-metadata/README.md](services/service-metadata/README.md) | +| `service-metadata-objects` | [services/service-metadata-objects/README.md](services/service-metadata-objects/README.md) | +| `service-metadata-server` | [services/service-metadata-server/README.md](services/service-metadata-server/README.md) | +| `service-api` | [services/service-api/README.md](services/service-api/README.md) | + +## 🤖 AI & Copilot Resources + +| Document | Description | +|----------|-------------| +| [.github/copilot-instructions.md](.github/copilot-instructions.md) | GitHub Copilot instructions for this project | +| [REPOSITORY_SUMMARY.md](REPOSITORY_SUMMARY.md) | AI-friendly repository summary | +| [QUICK_REFERENCE_CARD.md](QUICK_REFERENCE_CARD.md) | Quick reference for common patterns | + +## 📝 Code Quality & Maintenance + +| Document | Description | +|----------|-------------| +| [CODE_OPTIMIZATION_SUGGESTIONS.md](CODE_OPTIMIZATION_SUGGESTIONS.md) | Code optimization suggestions | +| [CODE_QUALITY_ANALYSIS.md](CODE_QUALITY_ANALYSIS.md) | Code quality analysis | +| [QUICK_ACTION_ITEMS.md](QUICK_ACTION_ITEMS.md) | Quick action items | + +## 🚀 Deployment & Operations + +| Document | Description | +|----------|-------------| +| [PUBLISH.md](PUBLISH.md) | Publishing and release process | +| [deploy/](deploy/) | Deployment configurations | +| [docker-compose.yml](docker-compose.yml) | Docker Compose setup | + +## 📚 Learning Paths + +### For Beginners + +1. Start with [README.md](README.md) - Understand what Steedos is +2. Read [docs/README.md](docs/README.md) - Documentation overview +3. Follow Quick Start in [docs/README.md](docs/README.md#-quick-start) +4. Learn [Metadata Basics](docs/metadata/README.md) +5. Try [ObjectQL Queries](docs/objectql/README.md) + +### For Developers + +1. Read [DEVELOPER_GUIDE.md](docs/DEVELOPER_GUIDE.md) - Setup environment +2. Understand [Architecture](docs/CORE_ARCHITECTURE_EN.md) +3. Study [Metadata System](docs/metadata/README.md) +4. Learn [ObjectQL](docs/objectql/README.md) in depth +5. Master [Triggers](docs/triggers/README.md) +6. Explore [Packages Index](docs/PACKAGES_INDEX.md) + +### For AI/LLM Training + +1. [REPOSITORY_SUMMARY.md](REPOSITORY_SUMMARY.md) - Complete overview +2. [.github/copilot-instructions.md](.github/copilot-instructions.md) - Project context +3. [docs/metadata/](docs/metadata/) - Complete metadata reference +4. [docs/objectql/](docs/objectql/) - Query language specification +5. [docs/triggers/](docs/triggers/) - Business logic patterns +6. [QUICK_REFERENCE_CARD.md](QUICK_REFERENCE_CARD.md) - Common patterns + +### For System Administrators + +1. [DEVELOPER_GUIDE.md](docs/DEVELOPER_GUIDE.md) - Environment setup +2. [docs/env.md](docs/env.md) - Environment variables +3. [deploy/](deploy/) - Deployment configurations +4. [PUBLISH.md](PUBLISH.md) - Release process + +## 🔍 Search by Topic + +### Metadata + +- [Metadata Overview](docs/metadata/README.md) +- [Metadata Types](docs/metadata/metadata-types.md) +- [Inheritance Rules](docs/metadata/inheritance-rules.md) +- [Object Definition](docs/metadata/object-metadata.md) +- [Field Types](docs/metadata/field-types.md) +- [Permissions](docs/metadata/permissions.md) + +### Queries + +- [ObjectQL Overview](docs/objectql/README.md) +- [Query Syntax](docs/objectql/query-syntax.md) +- [Filter Operators](docs/objectql/filter-operators.md) +- [Best Practices](docs/objectql/best-practices.md) + +### Business Logic + +- [Triggers Overview](docs/triggers/README.md) +- [Trigger Types](docs/triggers/trigger-types.md) +- [Trigger Context](docs/triggers/trigger-context.md) + +### Development + +- [Developer Guide](docs/DEVELOPER_GUIDE.md) +- [Architecture](docs/CORE_ARCHITECTURE_EN.md) +- [Packages Index](docs/PACKAGES_INDEX.md) +- [Quick Reference](docs/QUICK_REFERENCE.md) + +### Operations + +- [Environment Variables](docs/env.md) +- [Deployment](deploy/) +- [Publishing](PUBLISH.md) + +## 📊 Documentation Statistics + +| Category | Files | Lines of Documentation | +|----------|-------|------------------------| +| Core Documentation | 7 | ~5,000 | +| Metadata Documentation | 6 | ~6,000 | +| ObjectQL Documentation | 4 | ~4,000 | +| Trigger Documentation | 3 | ~3,000 | +| Package READMEs | 26 | ~2,000 | +| Service READMEs | 39 | ~3,000 | +| **Total** | **85+** | **~23,000+** | + +## 🗂️ File Organization + +``` +steedos-platform/ +├── README.md # Main README +├── README_cn.md # Chinese README +├── REPOSITORY_SUMMARY.md # Repository summary +├── QUICK_REFERENCE_CARD.md # Quick reference +├── CONTRIBUTING.md # Contribution guide +├── LICENSE.txt # License +├── .github/ +│ └── copilot-instructions.md # Copilot instructions +├── docs/ # Main documentation +│ ├── README.md # Documentation index +│ ├── CORE_ARCHITECTURE_EN.md # Architecture +│ ├── DEVELOPER_GUIDE.md # Developer guide +│ ├── PACKAGES_INDEX.md # Packages index +│ ├── QUICK_REFERENCE.md # Quick reference +│ ├── object-service.md # Object service +│ ├── env.md # Environment variables +│ ├── metadata/ # Metadata docs +│ │ ├── README.md +│ │ ├── metadata-types.md +│ │ ├── inheritance-rules.md +│ │ ├── object-metadata.md +│ │ ├── field-types.md +│ │ └── permissions.md +│ ├── objectql/ # ObjectQL docs +│ │ ├── README.md +│ │ ├── query-syntax.md +│ │ ├── filter-operators.md +│ │ └── best-practices.md +│ └── triggers/ # Trigger docs +│ ├── README.md +│ ├── trigger-types.md +│ └── trigger-context.md +├── packages/ # Core packages +│ └── [26 packages with READMEs] +└── services/ # Microservices + └── [39 services with READMEs] +``` + +## 🔗 External Resources + +| Resource | URL | +|----------|-----| +| Official Website | https://www.steedos.com/ | +| Online Documentation | https://docs.steedos.com/ | +| GitHub Repository | https://github.com/steedos/steedos-platform | +| Community Forum | https://github.com/steedos/steedos-platform/discussions | +| Issue Tracker | https://github.com/steedos/steedos-platform/issues | +| Example Projects | https://github.com/steedos/steedos-templates | +| Docker Hub | https://hub.docker.com/r/steedos/steedos-community | + +## 🆘 Getting Help + +If you can't find what you're looking for: + +1. **Search the documentation** - Use Ctrl+F or GitHub search +2. **Check examples** - Look at [example projects](https://github.com/steedos/steedos-templates) +3. **Ask the community** - Post in [GitHub Discussions](https://github.com/steedos/steedos-platform/discussions) +4. **Report issues** - Create an [issue](https://github.com/steedos/steedos-platform/issues) +5. **Contact support** - See [official website](https://www.steedos.com/) for contact info + +## 📅 Documentation Maintenance + +- **Last Major Update**: 2026-01-14 +- **Version**: 3.0.12 +- **Next Review**: Q2 2026 + +## 🤝 Contributing to Documentation + +We welcome documentation improvements! See [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines. + +### Documentation Needs + +- [ ] More code examples +- [ ] Video tutorials +- [ ] API reference documentation +- [ ] Migration guides +- [ ] Troubleshooting guides +- [ ] Performance tuning guides + +--- + +**Note**: This index is automatically updated. For the most current documentation, always refer to the files directly. diff --git a/QUICK_REFERENCE_CARD.md b/QUICK_REFERENCE_CARD.md new file mode 100644 index 0000000000..f6489df26c --- /dev/null +++ b/QUICK_REFERENCE_CARD.md @@ -0,0 +1,422 @@ +# Steedos Platform - Quick Reference Card + +## 🚀 Common Commands + +```bash +# Setup +yarn # Install dependencies +yarn build # Build all packages +yarn start # Start development server + +# Development +yarn dev # Start with watch mode +yarn test # Run tests +yarn lint # Check code style + +# Docker +yarn docker:db # Start MongoDB/Redis/NATS +docker build -t myapp . # Build Docker image +``` + +## 📝 Object Definition Template + +```yaml +# objects/my_object.object.yml +name: my_object +label: My Object +icon: custom1 +enable_search: true +enable_files: true +fields: + name: + type: text + label: Name + required: true + status: + type: select + label: Status + options: + - { label: Active, value: active } + - { label: Inactive, value: inactive } + defaultValue: active + related_to: + type: lookup + label: Related To + reference_to: accounts +list_views: + all: + label: All + columns: [name, status, created] + filters: [] +permission_set: + user: + allowCreate: true + allowRead: true + allowEdit: true +``` + +## 🔍 ObjectQL Cheat Sheet + +### Basic Query +```javascript +const records = await objects.my_object.find({ + fields: ['name', 'status'], + filters: [['status', '=', 'active']], + sort: 'name', + top: 100 +}, userSession); +``` + +### Filter Operators +```javascript +// Comparison +['amount', '>', 1000] +['amount', '>=', 1000] +['amount', '<', 5000] +['amount', '<=', 5000] +['amount', '=', 1000] +['amount', '!=', 0] + +// String +['name', 'contains', 'company'] +['name', 'startswith', 'A'] +['email', 'endswith', '@example.com'] + +// Range +['age', 'between', [20, 30]] + +// Array +['status', 'in', ['active', 'pending']] +['status', '=', ['active', 'pending']] // Same as in + +// Date +['created', '=', 'this_month'] +['created', '=', 'last_30_days'] +['created', 'between', ['2024-01-01', '2024-12-31']] + +// Null +['description', '=', null] +['description', '!=', null] + +// Logical +[ + ['status', '=', 'active'], + 'and', + ['amount', '>', 1000] +] + +[ + ['priority', '=', 'high'], + 'or', + ['amount', '>', 10000] +] +``` + +### CRUD Operations +```javascript +// Create +const newRecord = await objects.my_object.insert({ + name: 'New Record', + status: 'active' +}, userSession); + +// Read +const record = await objects.my_object.findOne(id, { + fields: ['name', 'status'] +}, userSession); + +// Update +await objects.my_object.update(id, { + status: 'inactive' +}, userSession); + +// Delete +await objects.my_object.delete(id, userSession); + +// Count +const count = await objects.my_object.count({ + filters: [['status', '=', 'active']] +}); + +// Aggregate +const result = await objects.my_object.aggregate({ + filters: [['status', '=', 'active']] +}, [ + { $group: { _id: '$category', total: { $sum: '$amount' } } } +]); +``` + +## ⚡ Trigger Template + +```javascript +// triggers/my_object.trigger.js +module.exports = { + listenTo: 'my_object', + + // Before Insert + beforeInsert: async function() { + const { doc } = this; + // Validate data + if (doc.amount < 0) { + throw new Error('Amount must be positive'); + } + // Auto-fill fields + doc.created_at = new Date(); + }, + + // After Insert + afterInsert: async function() { + const { doc, id } = this; + // Create related records + await this.getObject('tasks').insert({ + related_to: id, + name: `Task for ${doc.name}` + }); + }, + + // Before Update + beforeUpdate: async function() { + const { doc, id } = this; + // Get previous doc + const prev = await this.getObject(this.objectName).findOne(id); + // Check status change + if (doc.status && doc.status !== prev.status) { + // Validate transition + this.validateStatusChange(prev.status, doc.status); + } + }, + + // After Update + afterUpdate: async function() { + const { doc, previousDoc, id } = this; + // Detect changes + if (doc.status !== previousDoc.status) { + // Send notification + await this.broker.call('notifications.send', { + to: doc.owner, + message: `Status changed to ${doc.status}` + }); + } + }, + + // Before Delete + beforeDelete: async function() { + const { id } = this; + // Check dependencies + const count = await this.getObject('tasks').count({ + filters: [['related_to', '=', id]] + }); + if (count > 0) { + throw new Error('Cannot delete: has related records'); + } + }, + + // After Delete + afterDelete: async function() { + const { previousDoc } = this; + // Cleanup + await this.broker.call('files.deleteByRecord', { + recordId: previousDoc._id + }); + }, + + // Helper methods + validateStatusChange: function(oldStatus, newStatus) { + const valid = { + 'draft': ['active', 'cancelled'], + 'active': ['completed', 'cancelled'], + 'completed': [], + 'cancelled': [] + }; + if (!valid[oldStatus]?.includes(newStatus)) { + throw new Error(`Cannot change from ${oldStatus} to ${newStatus}`); + } + } +}; +``` + +## 🔐 Permission Set Template + +```yaml +# permissionsets/my_role.permissionset.yml +name: my_role +label: My Role +license: platform +object_permissions: + my_object: + allowCreate: true + allowRead: true + allowEdit: true + allowDelete: false + viewAllRecords: false + modifyAllRecords: false +field_permissions: + my_object.sensitive_field: + readable: false + editable: false + my_object.readonly_field: + readable: true + editable: false +``` + +## 🎨 Application Template + +```yaml +# applications/my_app.app.yml +_id: my_app +name: My Application +description: My application description +icon: apps +is_creator: true +visible: true +sort: 100 +objects: + - my_object + - accounts + - contacts +``` + +## 📋 Layout Template + +```yaml +# layouts/my_object_default.layout.yml +name: my_object_default +object_name: my_object +profiles: + - user + - admin +sections: + - label: Basic Information + columns: 2 + fields: + - name + - status + - owner + - label: Details + columns: 1 + fields: + - description + - related_to +``` + +## 🔧 Field Type Quick Reference + +| Type | Example | Use Case | +|------|---------|----------| +| `text` | `{ type: text }` | Short text | +| `textarea` | `{ type: textarea, rows: 5 }` | Long text | +| `number` | `{ type: number, scale: 2 }` | Numbers | +| `currency` | `{ type: currency }` | Money | +| `date` | `{ type: date }` | Date only | +| `datetime` | `{ type: datetime }` | Date and time | +| `boolean` | `{ type: boolean }` | Yes/No | +| `select` | `{ type: select, options: [...] }` | Dropdown | +| `lookup` | `{ type: lookup, reference_to: accounts }` | Relationship | +| `master_detail` | `{ type: master_detail, reference_to: accounts }` | Parent-child | +| `formula` | `{ type: formula, formula: "field1 + field2" }` | Calculated | +| `autonumber` | `{ type: autonumber, formula: "PRE-{0000}" }` | Auto ID | + +## 🌐 Environment Variables + +```bash +# Database +MONGO_URL=mongodb://localhost:27017/steedos +MONGO_OPLOG_URL=mongodb://localhost:27017/local + +# Server +ROOT_URL=http://localhost:5100 +PORT=5100 +NODE_ENV=development + +# Features +STEEDOS_TENANT_ENABLE_REGISTER=true +STEEDOS_CFS_STORE=local + +# Redis (optional) +REDIS_URL=redis://localhost:6379 + +# Storage +STEEDOS_STORAGE_DIR=storage +``` + +## 🐛 Debug Commands + +```bash +# Enable debug logging +export DEBUG=objectql:* # ObjectQL logs +export DEBUG=metadata:* # Metadata logs +export DEBUG=* # All logs + +# View logs +tail -f logs/app.log + +# Check metadata +curl http://localhost:5100/api/metadata/reload + +# Test API +curl http://localhost:5100/api/v4/objects/accounts +``` + +## 📚 Useful Paths + +| Resource | Path | +|----------|------| +| Objects | `objects/*.object.yml` | +| Triggers | `triggers/*.trigger.js` | +| Actions | `actions/*.action.js` | +| Apps | `applications/*.app.yml` | +| Permissions | `permissionsets/*.permissionset.yml` | +| Layouts | `layouts/*.layout.yml` | +| Translations | `translations/*.yml` | +| Documentation | `docs/` | + +## 🔗 Important URLs + +| URL | Purpose | +|-----|---------| +| http://localhost:5100 | Application UI | +| http://localhost:5100/api | API root | +| http://localhost:5100/graphql | GraphQL endpoint | +| http://localhost:5100/api/metadata | Metadata API | +| http://localhost:5100/steedos/api/odata/v4 | OData endpoint | + +## 💡 Best Practices + +### ✅ Do +- Pass `userSession` for permission checks +- Select only needed fields +- Use indexed fields in filters +- Limit query results with `top` +- Keep triggers simple +- Document complex logic +- Handle errors gracefully + +### ❌ Don't +- Query all fields when not needed +- Forget permission checks +- Use non-indexed fields in filters +- Create infinite loops in triggers +- Perform N+1 queries +- Hardcode values +- Ignore error handling + +## 🆘 Common Issues + +### Object not found +- Check object name spelling +- Verify metadata loaded +- Restart server + +### Permission denied +- Pass userSession parameter +- Check permission set config +- Verify user has access + +### Trigger not firing +- Check listenTo matches object name +- Verify file location +- Restart server after changes + +--- + +**For more details, see**: [Full Documentation](docs/README.md) diff --git a/REPOSITORY_SUMMARY.md b/REPOSITORY_SUMMARY.md new file mode 100644 index 0000000000..85725ea76f --- /dev/null +++ b/REPOSITORY_SUMMARY.md @@ -0,0 +1,522 @@ +# Steedos Platform - Repository Summary + +> **Last Updated**: 2026-01-14 +> **Version**: 3.0.12 +> **Purpose**: AI-Native Low-Code Platform for Enterprise Applications + +## 📋 Quick Facts + +| Aspect | Details | +|--------|---------| +| **Architecture** | Metadata-Driven, Microservices-based | +| **Primary Language** | JavaScript/TypeScript | +| **Backend Framework** | Node.js with Moleculer | +| **Frontend Framework** | React with Baidu Amis | +| **Databases Supported** | MongoDB, PostgreSQL, MySQL | +| **Core Packages** | 26 NPM packages | +| **Microservices** | 39 services | +| **Metadata Files** | 167+ object/trigger/action definitions | +| **License** | MIT | + +## 🎯 Project Mission + +Steedos Platform enables **10x faster enterprise application development** by combining: +1. **Generative AI** - Natural language to app generation +2. **Metadata-Driven Core** - Configuration over code +3. **Standard Technologies** - Open source, standard Node.js/React + +## 🏗️ Architecture Overview + +### Three-Layer Architecture + +``` +┌─────────────────────────────────────────┐ +│ Presentation Layer │ +│ (React + Amis Low-Code UI Framework) │ +└─────────────────────────────────────────┘ + ↓ +┌─────────────────────────────────────────┐ +│ Application Layer │ +│ (Moleculer Microservices + API) │ +└─────────────────────────────────────────┘ + ↓ +┌─────────────────────────────────────────┐ +│ Data Layer │ +│ (ObjectQL + MongoDB/PostgreSQL/MySQL) │ +└─────────────────────────────────────────┘ +``` + +### Key Components + +1. **Metadata Engine** (`@steedos/metadata-core`) + - Loads and parses metadata from YAML/JS files + - Handles metadata inheritance and merging + - Supports hot-reload for development + +2. **ObjectQL Engine** (`@steedos/objectql`) + - Cross-database query abstraction + - Permission enforcement + - Trigger execution + - Formula and validation processing + +3. **Object Service** (Moleculer services) + - Auto-generated microservices from metadata + - RESTful and GraphQL APIs + - Event-driven communication + - Horizontal scalability + +4. **Builder Application** (`builder6/`) + - Visual metadata editor + - Page designer + - Workflow designer + - AI-powered generation tools + +## 📦 Package Structure + +### Core Packages (`packages/`) + +| Package | Purpose | Key Features | +|---------|---------|--------------| +| `@steedos/objectql` | Query engine | Cross-DB queries, permissions, triggers | +| `@steedos/metadata-core` | Metadata engine | Load, parse, merge metadata | +| `@steedos/accounts` | Authentication | User management, SSO, OAuth | +| `@steedos/server` | Main server | Bootstrap, routing, middleware | +| `@steedos/api` | API layer | REST, GraphQL, OData endpoints | +| `@steedos/formula` | Formula engine | Field formulas, validation rules | +| `@steedos/filters` | Query filters | Filter parsing and execution | +| `@steedos/process-approval` | Workflow | Approval processes, routing | + +### Microservices (`services/`) + +| Service | Purpose | +|---------|---------| +| `service-metadata-objects` | Object metadata management | +| `service-metadata-server` | Metadata sync server | +| `service-api` | API Gateway | +| `service-pages` | Page rendering | +| `service-workflow` | Workflow engine | +| `standard-objects` | Standard object definitions | +| `standard-accounts` | Account management | +| `standard-space` | Workspace management | + +### Builder Application (`builder6/`) + +| Component | Purpose | +|-----------|---------| +| `server/` | Main application server | +| `webapp/` | React frontend application | +| `ai/` | AI integration services | + +## 🔑 Core Concepts + +### 1. Metadata Types + +```yaml +# Object Definition (.object.yml) +name: sales_order +label: Sales Order +fields: + order_number: + type: autonumber + formula: "SO-{0000}" + customer: + type: lookup + reference_to: accounts + amount: + type: currency + +# Application Definition (.app.yml) +name: sales +label: Sales Management +objects: + - accounts + - sales_orders + +# Permission Set (.permissionset.yml) +name: sales_manager +object_permissions: + sales_orders: + allowCreate: true + allowRead: true + +# Trigger (.trigger.js) +module.exports = { + listenTo: 'sales_orders', + beforeInsert: async function() { + // Validation logic + } +}; +``` + +### 2. ObjectQL Query Patterns + +```javascript +// Basic query +const records = await objects.accounts.find({ + fields: ['name', 'status'], + filters: [['status', '=', 'active']], + sort: 'name', + top: 100 +}, userSession); + +// Complex filters +filters: [ + ['status', '=', 'active'], + 'and', + [ + ['amount', '>', 10000], + 'or', + ['priority', '=', 'high'] + ] +]; + +// Relationship queries +fields: ['name', 'account.name', 'account.owner.name'] +``` + +### 3. Trigger Patterns + +```javascript +// Data validation +beforeInsert: async function() { + if (this.doc.amount < 0) { + throw new Error('Invalid amount'); + } +} + +// Auto-fill fields +beforeInsert: async function() { + this.doc.code = await generateCode(); +} + +// Create related records +afterInsert: async function() { + await this.getObject('tasks').insert({ + related_to: this.id + }); +} + +// Send notifications +afterUpdate: async function() { + if (this.doc.status !== this.previousDoc.status) { + await notifyOwner(this.id); + } +} +``` + +## 🔄 Data Flow + +### Creating a Record + +``` +1. User submits form + ↓ +2. API validates request + ↓ +3. ObjectQL checks permissions + ↓ +4. beforeInsert trigger executes + ↓ +5. Record saved to database + ↓ +6. afterInsert trigger executes + ↓ +7. Events published to message queue + ↓ +8. Response returned to user +``` + +### Querying Records + +``` +1. User requests data + ↓ +2. API validates request + ↓ +3. beforeFind trigger executes + ↓ +4. ObjectQL builds database query + ↓ +5. Permissions applied as filters + ↓ +6. Database executes query + ↓ +7. afterFind trigger executes + ↓ +8. Results returned to user +``` + +## 🛠️ Development Workflow + +### 1. Create Object Metadata + +```bash +# Create object definition +touch objects/custom_object.object.yml +``` + +### 2. Define Fields and Permissions + +Edit the YAML file with object structure. + +### 3. Create Triggers (if needed) + +```bash +touch triggers/custom_object.trigger.js +``` + +### 4. Test Locally + +```bash +yarn start +# Open http://localhost:5100 +``` + +### 5. Deploy + +```bash +# Build +yarn build + +# Deploy to production +docker build -t myapp . +docker push myapp +``` + +## 📊 Metadata Statistics + +| Metadata Type | Count | Location | +|---------------|-------|----------| +| Objects | 80+ | `*/objects/` | +| Triggers | 40+ | `*/triggers/` | +| Actions | 20+ | `*/actions/` | +| Applications | 10+ | `*/applications/` | +| Permission Sets | 15+ | `*/permissionsets/` | +| Layouts | 30+ | `*/layouts/` | + +## 🔐 Security Model + +### Permission Layers + +1. **Object-level**: Create, Read, Update, Delete permissions +2. **Field-level**: Read, Edit permissions per field +3. **Record-level**: Owner, sharing rules, role hierarchy +4. **API-level**: API key, OAuth token validation + +### Permission Evaluation + +```javascript +// Permissions are checked in this order: +1. Is user authenticated? +2. Does user have object-level permission? +3. Does user have field-level permission? +4. Does user own the record OR have viewAllRecords? +5. Is record shared with user? +``` + +## 🚀 Performance Optimization + +### Database Queries + +- Index frequently filtered fields +- Use `top` to limit results +- Select only needed fields +- Avoid N+1 queries in triggers + +### Metadata Loading + +- Lazy load metadata packages +- Cache parsed metadata +- Use metadata hot-reload in development only + +### Caching + +- Redis for session data +- In-memory cache for metadata +- CDN for static assets + +## 🧪 Testing Strategy + +### Unit Tests + +```javascript +// Test ObjectQL queries +describe('ObjectQL', () => { + it('should query with filters', async () => { + const results = await objects.accounts.find({ + filters: [['status', '=', 'active']] + }); + expect(results.length).toBeGreaterThan(0); + }); +}); +``` + +### Integration Tests + +```javascript +// Test triggers +describe('Triggers', () => { + it('should execute beforeInsert', async () => { + const doc = { name: 'Test' }; + await objects.accounts.insert(doc); + expect(doc.code).toBeDefined(); + }); +}); +``` + +### E2E Tests + +Using Playwright for UI testing. + +## 📈 Scaling Considerations + +### Horizontal Scaling + +- Stateless microservices +- Load balancer distributes requests +- Redis for session sharing +- MongoDB replica sets + +### Vertical Scaling + +- Increase Node.js memory: `--max-old-space-size=4096` +- Optimize database indexes +- Use database read replicas + +### Caching Strategy + +- Object metadata: In-memory cache +- User sessions: Redis +- Query results: Application-level cache +- Static assets: CDN + +## 🔍 Monitoring and Debugging + +### Logging + +```javascript +// Enable debug logging +export DEBUG=objectql:*,metadata:* + +// Log levels +logger.trace('Detailed trace'); +logger.debug('Debug info'); +logger.info('Info message'); +logger.warn('Warning'); +logger.error('Error', error); +``` + +### Performance Monitoring + +- Moleculer metrics +- Database slow query log +- APM tools (New Relic, DataDog) + +### Error Tracking + +- Sentry integration +- Custom error handlers +- Audit logs + +## 🌐 Internationalization + +### Supported Languages + +- English (en) +- Chinese (zh-CN) +- Extensible to other languages + +### Translation Files + +```yaml +# translations/custom.en.yml +custom_object: + label: Custom Object + fields: + name: Name + status: Status +``` + +## 🔄 CI/CD Pipeline + +### GitHub Actions Workflows + +1. **Lint**: ESLint, Prettier +2. **Test**: Unit and integration tests +3. **Build**: Compile TypeScript, bundle assets +4. **Deploy**: Docker image build and push + +### Deployment Targets + +- Docker containers +- Kubernetes clusters +- Cloud platforms (AWS, Azure, GCP) +- On-premises servers + +## 📚 Learning Resources + +### For Beginners + +1. [Quick Start Guide](docs/README.md#-quick-start) +2. [Metadata Basics](docs/metadata/README.md) +3. [ObjectQL Tutorial](docs/objectql/README.md) + +### For Developers + +1. [Developer Guide](docs/DEVELOPER_GUIDE.md) +2. [Architecture Documentation](docs/CORE_ARCHITECTURE_EN.md) +3. [API Reference](docs/QUICK_REFERENCE.md) + +### For Advanced Users + +1. [Metadata Inheritance](docs/metadata/inheritance-rules.md) +2. [Trigger Development](docs/triggers/trigger-types.md) +3. [Performance Optimization](docs/objectql/best-practices.md) + +## 🤝 Contributing + +### Code Contributions + +1. Fork the repository +2. Create feature branch +3. Follow coding standards +4. Write tests +5. Submit pull request + +### Documentation Contributions + +1. Improve existing docs +2. Add examples +3. Translate to other languages +4. Report errors + +## 🔮 Roadmap + +### Current Focus (Q1 2026) + +- Enhanced AI code generation +- Improved metadata editor +- Performance optimizations +- More database drivers + +### Future Plans + +- Real-time collaboration +- Advanced analytics +- Mobile app builder +- More AI integrations + +## 📞 Support + +- **Documentation**: [docs.steedos.com](https://docs.steedos.com) +- **Community**: [GitHub Discussions](https://github.com/steedos/steedos-platform/discussions) +- **Issues**: [GitHub Issues](https://github.com/steedos/steedos-platform/issues) +- **Website**: [www.steedos.com](https://www.steedos.com) + +## 📄 License + +MIT License - See [LICENSE.txt](LICENSE.txt) + +--- + +**For AI Assistants**: This repository uses a metadata-driven architecture. When helping with code, always consider the metadata definitions first, then implement custom logic only when configuration isn't sufficient. From dee40c8aaedb5b7b7d3c4d45a0d7542f67528df3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 14 Jan 2026 05:56:04 +0000 Subject: [PATCH 4/4] Add field visibility control documentation with visible_on attribute Co-authored-by: baozhoutao <6194462+baozhoutao@users.noreply.github.com> --- .github/copilot-instructions.md | 27 +++++++++++++-- QUICK_REFERENCE_CARD.md | 19 +++++++++++ REPOSITORY_SUMMARY.md | 17 +++++++++ docs/metadata/field-types.md | 27 +++++++++++++++ docs/metadata/inheritance-rules.md | 30 ++++++++++++++++ docs/metadata/object-metadata.md | 55 ++++++++++++++++++++++++++++++ 6 files changed, 173 insertions(+), 2 deletions(-) diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 2c4146f87f..c0b93ef2cf 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -201,7 +201,9 @@ When multiple packages define the same object: 4. **Objects deep merge** - Object properties merge recursively 5. **Triggers replace** - Later triggers completely replace earlier ones -Example: +### Field Property Override + +**Important**: Object and field properties can be overridden in extension packages. ```yaml # Base package @@ -210,16 +212,37 @@ fields: phone: type: text required: false + hidden: true # Extension package (loaded later) name: accounts fields: phone: - required: true # Override + required: true # Override required + visible_on: '{{true}}' # Make hidden field visible industry: # Add new field type: select ``` +### Field Visibility Control + +**Critical**: Field visibility is primarily controlled through the `visible_on` attribute: + +- To make a hidden field visible: Add `visible_on: '{{true}}'` +- To conditionally show field: Use `visible_on: '{{formData.status === "active"}}'` +- If `visible_on` already exists, modify the expression instead of adding a new one +- The `visible_on` attribute takes precedence over the `hidden` attribute + +Example: +```yaml +# Make a hidden field visible +fields: + previously_hidden: + type: text + label: Now Visible + visible_on: '{{true}}' # Add this to show the field +``` + ## Testing ```bash diff --git a/QUICK_REFERENCE_CARD.md b/QUICK_REFERENCE_CARD.md index f6489df26c..bb5478c0e3 100644 --- a/QUICK_REFERENCE_CARD.md +++ b/QUICK_REFERENCE_CARD.md @@ -43,6 +43,11 @@ fields: type: lookup label: Related To reference_to: accounts + hidden_field: + type: text + label: Hidden Field + hidden: true + visible_on: '{{false}}' # Hidden by default list_views: all: label: All @@ -55,6 +60,20 @@ permission_set: allowEdit: true ``` +### Field Visibility Override + +```yaml +# In extension package to make hidden field visible +fields: + hidden_field: + visible_on: '{{true}}' # Make visible + + conditional_field: + visible_on: '{{formData.status === "active"}}' # Conditional +``` + +**Important**: Field visibility is primarily controlled by `visible_on` attribute. To show a hidden field, add `visible_on: '{{true}}'`. + ## 🔍 ObjectQL Cheat Sheet ### Basic Query diff --git a/REPOSITORY_SUMMARY.md b/REPOSITORY_SUMMARY.md index 85725ea76f..4fdf5adfa9 100644 --- a/REPOSITORY_SUMMARY.md +++ b/REPOSITORY_SUMMARY.md @@ -124,6 +124,17 @@ fields: reference_to: accounts amount: type: currency + internal_notes: + type: textarea + label: Internal Notes + hidden: true + visible_on: '{{false}}' # Hidden by default + +# Field Override (in extension package) +# To make hidden field visible, add visible_on +fields: + internal_notes: + visible_on: '{{true}}' # Override to make visible # Application Definition (.app.yml) name: sales @@ -148,6 +159,12 @@ module.exports = { }; ``` +**Important Field Property Override Rules**: +- Object and field properties can be overridden in extension packages +- Field visibility is primarily controlled through `visible_on` attribute +- To make a hidden field visible: Add `visible_on: '{{true}}'` +- If `visible_on` already exists, modify the expression instead of adding new one + ### 2. ObjectQL Query Patterns ```javascript diff --git a/docs/metadata/field-types.md b/docs/metadata/field-types.md index b03cb6900a..b43d295980 100644 --- a/docs/metadata/field-types.md +++ b/docs/metadata/field-types.md @@ -670,6 +670,7 @@ field_name: readonly: false # 是否只读 hidden: false # 是否隐藏 omit: false # 是否排除 + visible_on: null # 字段可见性控制表达式 group: "分组名" # 字段分组 sortable: true # 是否可排序 searchable: true # 是否可搜索 @@ -679,6 +680,32 @@ field_name: defaultValue: null # 默认值 ``` +### 字段可见性控制 + +字段的显示和隐藏主要通过 `visible_on` 属性控制: + +```yaml +# 始终显示字段 +field_name: + type: text + label: 字段名 + visible_on: '{{true}}' + +# 条件显示字段(根据其他字段值) +conditional_field: + type: text + label: 条件字段 + visible_on: '{{formData.status === "active"}}' + +# 隐藏字段 +hidden_field: + type: text + label: 隐藏字段 + visible_on: '{{false}}' +``` + +**重要**: 如果需要将隐藏的字段调整为显示状态,应添加 `visible_on: '{{true}}'`,除非已有其他 `visible_on` 配置。 + ## 字段命名规范 1. **使用小写字母和下划线** diff --git a/docs/metadata/inheritance-rules.md b/docs/metadata/inheritance-rules.md index e7b5073858..4db3330819 100644 --- a/docs/metadata/inheritance-rules.md +++ b/docs/metadata/inheritance-rules.md @@ -148,6 +148,36 @@ fields: type: text label: 电话 required: false + hidden: true +``` + +**扩展包**: +```yaml +name: accounts +fields: + phone: + required: true # 只覆盖 required 属性 + label: 联系电话 # 覆盖 label 属性 + visible_on: '{{true}}' # 将隐藏字段设为显示 +``` + +**合并结果**: +```yaml +name: accounts +fields: + phone: + type: text # 保留原值 + label: 联系电话 # 使用新值 + required: true # 使用新值 + hidden: true # 保留原值 + visible_on: '{{true}}' # 新增属性,控制字段显示 +``` + +**字段可见性控制**: +- 字段的显示/隐藏主要通过 `visible_on` 属性控制 +- 如果需要将隐藏字段改为显示,添加 `visible_on: '{{true}}'` +- 如果已有 `visible_on` 配置,需要修改其表达式而非添加新的 +- `hidden` 属性和 `visible_on` 可以同时存在,`visible_on` 优先级更高 ``` **扩展包**: diff --git a/docs/metadata/object-metadata.md b/docs/metadata/object-metadata.md index 5baf92fdcf..1cf141d743 100644 --- a/docs/metadata/object-metadata.md +++ b/docs/metadata/object-metadata.md @@ -132,6 +132,61 @@ fields: - label: 已提交 value: submitted defaultValue: draft + + # 隐藏字段(通过 visible_on 控制) + internal_notes: + type: textarea + label: 内部备注 + visible_on: '{{false}}' # 默认隐藏 +``` + +### 字段可见性控制 + +字段的显示和隐藏主要通过 `visible_on` 属性控制: + +```yaml +fields: + # 始终显示 + always_visible: + type: text + label: 总是显示 + visible_on: '{{true}}' + + # 条件显示 + conditional_field: + type: text + label: 条件字段 + visible_on: '{{formData.status === "approved"}}' + + # 默认隐藏 + hidden_field: + type: text + label: 隐藏字段 + visible_on: '{{false}}' +``` + +**重要**: +- 如果需要将隐藏字段改为显示,添加 `visible_on: '{{true}}'` +- 如果已有 `visible_on` 配置,修改其表达式而非添加新的 +- `visible_on` 优先级高于 `hidden` 属性 + +### 字段属性覆盖 + +在扩展包中可以覆盖字段属性: + +```yaml +# 基础包 +fields: + phone: + type: text + label: 电话 + hidden: true + +# 扩展包 - 将隐藏字段设为显示 +fields: + phone: + visible_on: '{{true}}' # 覆盖显示状态 + label: 联系电话 # 覆盖标签 ``` ### 独立字段文件