openxiangda 1.0.34 → 1.0.36

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/README.md +18 -0
  2. package/lib/cli.js +410 -1
  3. package/lib/workspace-init.js +1 -0
  4. package/openxiangda-skills/SKILL.md +5 -3
  5. package/openxiangda-skills/references/best-practices.md +11 -6
  6. package/openxiangda-skills/references/component-guide.md +10 -11
  7. package/openxiangda-skills/references/connector-resources.md +3 -0
  8. package/openxiangda-skills/references/data-views.md +217 -0
  9. package/openxiangda-skills/references/forms/component-registry.md +4 -3
  10. package/openxiangda-skills/references/forms/form-schema.md +31 -2
  11. package/openxiangda-skills/references/pages/page-sdk.md +43 -0
  12. package/openxiangda-skills/references/style-system.md +14 -18
  13. package/openxiangda-skills/references/troubleshooting.md +13 -13
  14. package/openxiangda-skills/references/workspace-state.md +9 -0
  15. package/openxiangda-skills/skills/openxiangda-form/SKILL.md +3 -3
  16. package/openxiangda-skills/skills/openxiangda-page/SKILL.md +2 -2
  17. package/openxiangda-skills/skills/openxiangda-permission-settings/SKILL.md +1 -1
  18. package/package.json +1 -1
  19. package/packages/sdk/dist/components/index.cjs +944 -765
  20. package/packages/sdk/dist/components/index.cjs.map +1 -1
  21. package/packages/sdk/dist/components/index.d.mts +18 -2
  22. package/packages/sdk/dist/components/index.d.ts +18 -2
  23. package/packages/sdk/dist/components/index.mjs +938 -761
  24. package/packages/sdk/dist/components/index.mjs.map +1 -1
  25. package/packages/sdk/dist/runtime/index.cjs +114 -30
  26. package/packages/sdk/dist/runtime/index.cjs.map +1 -1
  27. package/packages/sdk/dist/runtime/index.d.mts +18 -1
  28. package/packages/sdk/dist/runtime/index.d.ts +18 -1
  29. package/packages/sdk/dist/runtime/index.mjs +114 -30
  30. package/packages/sdk/dist/runtime/index.mjs.map +1 -1
  31. package/packages/sdk/dist/styles/antd-theme.cjs +11 -3
  32. package/packages/sdk/dist/styles/antd-theme.cjs.map +1 -1
  33. package/packages/sdk/dist/styles/antd-theme.d.mts +2 -1
  34. package/packages/sdk/dist/styles/antd-theme.d.ts +2 -1
  35. package/packages/sdk/dist/styles/antd-theme.mjs +11 -3
  36. package/packages/sdk/dist/styles/antd-theme.mjs.map +1 -1
  37. package/packages/sdk/dist/styles/tailwind-preset.cjs +0 -1
  38. package/packages/sdk/dist/styles/tailwind-preset.cjs.map +1 -1
  39. package/packages/sdk/dist/styles/tailwind-preset.d.mts +0 -1
  40. package/packages/sdk/dist/styles/tailwind-preset.d.ts +0 -1
  41. package/packages/sdk/dist/styles/tailwind-preset.mjs +0 -1
  42. package/packages/sdk/dist/styles/tailwind-preset.mjs.map +1 -1
  43. package/packages/sdk/dist/styles/tokens.css +1 -0
  44. package/packages/sdk/src/build-source/scripts/build-forms.mjs +135 -50
  45. package/packages/sdk/src/build-source/scripts/build-pages.mjs +37 -10
  46. package/packages/sdk/src/build-source/scripts/register.mjs +2 -0
  47. package/packages/sdk/src/build-source/scripts/utils/form-api.mjs +1 -0
  48. package/packages/sdk/src/build-source/scripts/utils/load-config.mjs +3 -2
  49. package/packages/sdk/src/build-source/scripts/utils/register-payload.test.ts +2 -1
  50. package/packages/sdk/src/build-source/scripts/utils/tailwind-config.mjs +9 -7
  51. package/packages/sdk/src/build-source/scripts/utils/tailwind-config.test.ts +6 -4
  52. package/packages/sdk/src/build-source/src/cli.mjs +17 -0
  53. package/templates/sy-lowcode-app-workspace/app-workspace.config.ts +3 -3
  54. package/templates/sy-lowcode-app-workspace/examples/best-practices/decision-guide.md +4 -3
  55. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/domain/role-governance/permissions.test.ts +1 -1
  56. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/forms/app-role/schema.ts +36 -18
  57. package/templates/sy-lowcode-app-workspace/examples/best-practices/src/forms/service-ticket/schema.ts +36 -18
  58. package/templates/sy-lowcode-app-workspace/postcss.config.cjs +0 -15
  59. package/templates/sy-lowcode-app-workspace/src/main.tsx +1 -12
  60. package/templates/sy-lowcode-app-workspace/src/shared/form-schema.ts +0 -1
package/README.md CHANGED
@@ -29,6 +29,8 @@ openxiangda automation executions daily_ticket_digest --profile dev
29
29
  openxiangda automation diagnose daily_ticket_digest --profile dev
30
30
  openxiangda permission role-list --profile dev
31
31
  openxiangda settings get customer --profile dev
32
+ openxiangda data-view list --profile dev
33
+ openxiangda data-view status ticket_with_customer --profile dev
32
34
  openxiangda resource validate --profile dev
33
35
  openxiangda resource plan --profile dev
34
36
  openxiangda resource publish --profile dev
@@ -79,6 +81,8 @@ Use `openxiangda skill install --dest <skills-dir>` to target a different Codex
79
81
 
80
82
  Create a new publishable app workspace with `openxiangda workspace init <dir>`. The command writes a minimal `sy-lowcode-app-workspace` template with React, Ant Design, the single `openxiangda` package, and the required `publish:all` / `openxiangda:publish` scripts. Use `--install` to run `pnpm install` immediately, or run it manually after creation.
81
83
 
84
+ New OpenXiangda code pages and form custom pages publish with `cssIsolation: "none"` by default, so Tailwind utilities and normal Ant Design styles apply without a `.sy-app-workspace` prefix. Explicit `namespace` and `shadow` settings are kept only for legacy compatibility.
85
+
82
86
  Local workspace state is authoritative. If the current folder has no `.openxiangda/state.json` app binding, create a new app instead of searching the platform for a similar app name:
83
87
 
84
88
  ```bash
@@ -96,6 +100,20 @@ openxiangda workspace bind --profile dev --app-type APP_XXXX
96
100
 
97
101
  工程化资源放在工作区 `src/resources/` 下,由 `openxiangda resource validate|plan|publish|pull` 管理。`workspace publish` 会先构建并注册 workspace 表单/页面,再执行非破坏性资源 upsert,这样菜单、权限组、流程和表单设置可以解析最新的 profile-local ID。需要删除平台中 manifest 未声明的资源时,显式传 `--prune`。连接器页面运行时通过 `sdk.connector.invoke()` / `sdk.connector.call("connector.api")` 调用平台运行时接口,第三方密钥只保存在后端连接器配置中。
98
102
 
103
+ 多表只读查询优先声明 `src/resources/data-views/*.json` 数据视图,而不是在页面里手写多次单表查询再拼数据。数据视图适合工单+客户、订单+商品、项目+成员、报表列表、跨页面复用查询等读多写少场景。发布时 CLI 会把 `formCode` 解析为当前 profile 的 `formUuid`,平台创建 PostgreSQL materialized view;页面通过 `sdk.dataView.query(code, params)` 或 `sdk.dataSource.run()` 查询输出字段。数据视图只读,刷新后才反映源表变化,不适合单表 CRUD、写回源表、强实时状态或简单 linkedForm 下拉。
104
+
105
+ 常用数据视图命令:
106
+
107
+ ```bash
108
+ openxiangda resource validate --profile dev
109
+ openxiangda resource plan --profile dev
110
+ openxiangda resource publish --profile dev
111
+ openxiangda data-view list --profile dev
112
+ openxiangda data-view status ticket_with_customer --profile dev
113
+ openxiangda data-view refresh ticket_with_customer --profile dev
114
+ openxiangda data-view query ticket_with_customer --query-json query.json --profile dev
115
+ ```
116
+
99
117
  AI-authored automation can use code-first resources:
100
118
 
101
119
  - Source: `src/automations/<resourceCode>/index.ts`
package/lib/cli.js CHANGED
@@ -54,6 +54,7 @@ async function main(argv) {
54
54
  if (command === 'menu') return menu(rest);
55
55
  if (command === 'workflow') return workflow(rest);
56
56
  if (command === 'automation') return automation(rest);
57
+ if (command === 'data-view') return dataView(rest);
57
58
  if (command === 'permission') return permission(rest);
58
59
  if (command === 'settings') return settings(rest);
59
60
  if (command === 'resource') return resource(rest);
@@ -105,6 +106,7 @@ Usage:
105
106
  openxiangda automation logs <instanceId> [--automation <automationCode|automationId>] [--summary] [--redact] [--json]
106
107
  openxiangda automation diagnose <automationCode|automationId> [--redact] [--json]
107
108
  openxiangda automation publish|enable|disable <automationCode|automationId>
109
+ openxiangda data-view list|status|refresh|query <dataViewCode> [--profile name] [--json]
108
110
  openxiangda permission role-list|role-create|role-bind
109
111
  openxiangda permission page-group-list|page-group-create|page-group-bind
110
112
  openxiangda permission form-group-list|form-group-create|form-group-bind
@@ -1090,6 +1092,7 @@ async function workspace(args) {
1090
1092
  menus: {},
1091
1093
  roles: {},
1092
1094
  connectors: {},
1095
+ dataViews: {},
1093
1096
  pagePermissionGroups: {},
1094
1097
  formPermissionGroups: {},
1095
1098
  formSettings: {},
@@ -1479,7 +1482,7 @@ async function page(args) {
1479
1482
  jsUrls: splitList(flags['js-urls']),
1480
1483
  framework: flags.framework || 'react',
1481
1484
  frameworkVersion: flags['framework-version'] || '',
1482
- cssIsolation: flags['css-isolation'] || 'namespace',
1485
+ cssIsolation: flags['css-isolation'] || 'none',
1483
1486
  format: flags.format || 'esm',
1484
1487
  },
1485
1488
  menu: flags.menu
@@ -1981,6 +1984,75 @@ async function automation(args) {
1981
1984
  fail('用法: openxiangda automation list|create|bind|pull|executions|logs|diagnose|publish|unpublish|enable|disable|delete|validate|cron-validate');
1982
1985
  }
1983
1986
 
1987
+ async function dataView(args) {
1988
+ const [subcommand, ...rest] = args;
1989
+ const { flags, positional } = parseArgs(rest);
1990
+ const config = loadConfig();
1991
+ const profileName = flags.profile || config.currentProfile;
1992
+ const target = getWorkspaceTarget(config, profileName, flags);
1993
+
1994
+ if (subcommand === 'list') {
1995
+ const data = await requestWithAuth(
1996
+ config,
1997
+ target.profileName,
1998
+ apiPathWithQuery(`/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/data-views`, {
1999
+ page: flags.page,
2000
+ pageSize: flags['page-size'] || flags.limit,
2001
+ code: flags.code,
2002
+ })
2003
+ );
2004
+ if (flags.json) return writeJson(data);
2005
+ print(JSON.stringify(data, null, 2));
2006
+ return;
2007
+ }
2008
+
2009
+ if (subcommand === 'status') {
2010
+ const [code] = positional;
2011
+ if (!code) fail('用法: openxiangda data-view status <dataViewCode>');
2012
+ const data = await requestWithAuth(
2013
+ config,
2014
+ target.profileName,
2015
+ `/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/data-views/${encodeURIComponent(code)}/status`
2016
+ );
2017
+ if (flags.json) return writeJson(data);
2018
+ print(JSON.stringify(data, null, 2));
2019
+ return;
2020
+ }
2021
+
2022
+ if (subcommand === 'refresh') {
2023
+ const [code] = positional;
2024
+ if (!code) fail('用法: openxiangda data-view refresh <dataViewCode>');
2025
+ const data = await requestWithAuth(
2026
+ config,
2027
+ target.profileName,
2028
+ `/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/data-views/${encodeURIComponent(code)}/refresh`,
2029
+ { method: 'POST' }
2030
+ );
2031
+ if (flags.json) return writeJson(data);
2032
+ print(JSON.stringify(data, null, 2));
2033
+ return;
2034
+ }
2035
+
2036
+ if (subcommand === 'query') {
2037
+ const [code] = positional;
2038
+ if (!code) fail('用法: openxiangda data-view query <dataViewCode> [--query-json file]');
2039
+ const data = await requestWithAuth(
2040
+ config,
2041
+ target.profileName,
2042
+ `/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/data-views/${encodeURIComponent(code)}/query`,
2043
+ {
2044
+ method: 'POST',
2045
+ body: buildDataViewQueryBody(flags),
2046
+ }
2047
+ );
2048
+ if (flags.json) return writeJson(data);
2049
+ print(JSON.stringify(data, null, 2));
2050
+ return;
2051
+ }
2052
+
2053
+ fail('用法: openxiangda data-view list|status|refresh|query');
2054
+ }
2055
+
1984
2056
  async function permission(args) {
1985
2057
  const [subcommand, ...rest] = args;
1986
2058
  const { flags, positional } = parseArgs(rest);
@@ -2601,6 +2673,7 @@ async function commands(args) {
2601
2673
  'menu list|create|bind|delete',
2602
2674
  'workflow list|create|bind|pull|publish|delete|validate',
2603
2675
  'automation list|create|bind|pull|publish|unpublish|enable|disable|delete|validate|cron-validate',
2676
+ 'data-view list|status|refresh|query',
2604
2677
  'permission role-list|role-create|role-bind|role-users|role-add-users',
2605
2678
  'permission page-group-list|page-group-create|page-group-bind',
2606
2679
  'permission form-group-list|form-group-create|form-group-bind|form-summary|menu-permissions',
@@ -2747,6 +2820,7 @@ function ensureResourceBuckets(bound) {
2747
2820
  bound.resources.menus = bound.resources.menus || {};
2748
2821
  bound.resources.roles = bound.resources.roles || {};
2749
2822
  bound.resources.connectors = bound.resources.connectors || {};
2823
+ bound.resources.dataViews = bound.resources.dataViews || {};
2750
2824
  bound.resources.notifications = bound.resources.notifications || {};
2751
2825
  bound.resources.notifications.templates = bound.resources.notifications.templates || {};
2752
2826
  bound.resources.notifications.typeConfigs = bound.resources.notifications.typeConfigs || {};
@@ -2939,6 +3013,14 @@ function saveAutomationResource(target, automationCode, automationId, extra = {}
2939
3013
  }, keys);
2940
3014
  }
2941
3015
 
3016
+ function saveDataViewResource(target, dataViewCode, dataViewId, extra = {}) {
3017
+ const keys = ['dataViewId', 'materializedViewName', 'status'];
3018
+ saveStateResource(target, 'dataViews', dataViewCode, {
3019
+ ...pickStateFields(extra, keys),
3020
+ dataViewId,
3021
+ }, keys);
3022
+ }
3023
+
2942
3024
  function saveRoleResource(target, roleCode, roleId) {
2943
3025
  saveStateResource(target, 'roles', roleCode, { roleId }, ['roleId']);
2944
3026
  }
@@ -3044,6 +3126,7 @@ const RESOURCE_SPECS = [
3044
3126
  { key: 'menus', dir: 'menus', topFiles: ['menus.json'], pluralKeys: ['menus'] },
3045
3127
  { key: 'workflows', dir: 'workflows', topFiles: ['workflows.json'], pluralKeys: ['workflows'] },
3046
3128
  { key: 'automations', dir: 'automations', topFiles: ['automations.json'], pluralKeys: ['automations'] },
3129
+ { key: 'dataViews', dir: 'data-views', topFiles: ['data-views.json'], pluralKeys: ['dataViews', 'data-views'] },
3047
3130
  {
3048
3131
  key: 'pagePermissionGroups',
3049
3132
  dir: path.join('permissions', 'page-groups'),
@@ -3254,6 +3337,14 @@ function validateResourceItem(kind, item, errors, warnings) {
3254
3337
  errors.push(`${label}: 缺少 definitionJson 或 definitionFile`);
3255
3338
  }
3256
3339
  }
3340
+ if (kind === 'dataViews') {
3341
+ const definition = item.definition || item;
3342
+ if (!item.name && !definition.name) errors.push(`${label}: 缺少 name`);
3343
+ if (!definition.base) errors.push(`${label}: 缺少 base`);
3344
+ if (!Array.isArray(definition.select) || definition.select.length === 0) {
3345
+ errors.push(`${label}: 缺少 select`);
3346
+ }
3347
+ }
3257
3348
  if (kind === 'pagePermissionGroups' && !item.name) errors.push(`${label}: 缺少 name`);
3258
3349
  if (kind === 'formPermissionGroups') {
3259
3350
  if (!item.name) errors.push(`${label}: 缺少 name`);
@@ -3542,6 +3633,7 @@ async function buildResourcePlan(config, target, manifest) {
3542
3633
  addNotificationPlanActions(target, actions, manifest.notifications || [], existing);
3543
3634
  await addWorkflowPlanActions(config, target, actions, manifest.workflows, existing.workflows);
3544
3635
  await addAutomationPlanActions(config, target, actions, manifest.automations, existing.automations);
3636
+ addPlanActions(actions, 'dataView', manifest.dataViews, existing.dataViews, (item, current) => dataViewEquals(target.bound, item, current));
3545
3637
  addPlanActions(actions, 'pagePermissionGroup', manifest.pagePermissionGroups, existing.pagePermissionGroups, (item, current) => pagePermissionGroupEquals(target.bound, item, current));
3546
3638
  addPlanActions(actions, 'formPermissionGroup', manifest.formPermissionGroups, existing.formPermissionGroups, (item, current) => formPermissionGroupEquals(target.bound, item, current));
3547
3639
  for (const item of manifest.formSettings || []) {
@@ -3575,6 +3667,7 @@ async function publishResourceManifest(config, target, manifest, options = {}) {
3575
3667
  await publishNotificationResources(config, target, manifest.notifications || [], result);
3576
3668
  await publishWorkflowResources(config, target, manifest.workflows || [], result);
3577
3669
  await publishAutomationResources(config, target, manifest.automations || [], result);
3670
+ await publishDataViewResources(config, target, manifest.dataViews || [], result);
3578
3671
  await publishPagePermissionGroupResources(config, target, manifest.pagePermissionGroups || [], result);
3579
3672
  await publishFormPermissionGroupResources(config, target, manifest.formPermissionGroups || [], result);
3580
3673
  if (options.prune) {
@@ -3596,6 +3689,7 @@ async function fetchExistingResourceMaps(config, target, manifest) {
3596
3689
  notificationTypeConfigs: new Map(),
3597
3690
  workflows: new Map(),
3598
3691
  automations: new Map(),
3692
+ dataViews: new Map(),
3599
3693
  pagePermissionGroups: new Map(),
3600
3694
  formPermissionGroups: new Map(),
3601
3695
  };
@@ -3674,6 +3768,37 @@ async function fetchExistingResourceMaps(config, target, manifest) {
3674
3768
  );
3675
3769
  indexByCode(maps.automations, normalizeItems(data), item => item.resourceCode);
3676
3770
  }
3771
+ if ((manifest.dataViews || []).length > 0) {
3772
+ const data = await requestWithAuth(
3773
+ config,
3774
+ target.profileName,
3775
+ apiPathWithQuery(`/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/data-views`, {
3776
+ page: 1,
3777
+ pageSize: 1000,
3778
+ })
3779
+ );
3780
+ indexByCode(maps.dataViews, normalizeItems(data), item => item.code || item.resourceCode);
3781
+ for (const item of manifest.dataViews || []) {
3782
+ const code = item.code || item.resourceCode;
3783
+ const existing = maps.dataViews.get(code);
3784
+ if (!existing) continue;
3785
+ const detail = await requestOptionalWithAuth(
3786
+ config,
3787
+ target.profileName,
3788
+ `/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/data-views/${encodeURIComponent(code)}`
3789
+ );
3790
+ const groups = await requestOptionalWithAuth(
3791
+ config,
3792
+ target.profileName,
3793
+ `/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/data-views/${encodeURIComponent(code)}/permission-groups`
3794
+ );
3795
+ maps.dataViews.set(code, {
3796
+ ...existing,
3797
+ ...(detail || {}),
3798
+ permissionGroups: normalizeItems(groups),
3799
+ });
3800
+ }
3801
+ }
3677
3802
  if ((manifest.pagePermissionGroups || []).length > 0) {
3678
3803
  const data = await requestWithAuth(
3679
3804
  config,
@@ -4237,6 +4362,52 @@ async function publishAutomationResources(config, target, automations, result) {
4237
4362
  }
4238
4363
  }
4239
4364
 
4365
+ async function publishDataViewResources(config, target, dataViews, result) {
4366
+ for (const dataViewItem of dataViews) {
4367
+ const existing = await findExistingDataView(config, target, dataViewItem.code);
4368
+ const body = normalizeDataViewManifest(target.bound, dataViewItem);
4369
+ const data = existing
4370
+ ? await requestWithAuth(
4371
+ config,
4372
+ target.profileName,
4373
+ `/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/data-views/${encodeURIComponent(dataViewItem.code)}`,
4374
+ { method: 'PUT', body }
4375
+ )
4376
+ : await requestWithAuth(
4377
+ config,
4378
+ target.profileName,
4379
+ `/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/data-views`,
4380
+ { method: 'POST', body }
4381
+ );
4382
+ if (data?.id) {
4383
+ saveDataViewResource(target, dataViewItem.code, data.id, {
4384
+ materializedViewName: data.materializedViewName,
4385
+ status: data.status,
4386
+ });
4387
+ }
4388
+ result.published.push({
4389
+ kind: 'dataView',
4390
+ code: dataViewItem.code,
4391
+ action: existing ? 'update' : 'create',
4392
+ id: data?.id,
4393
+ });
4394
+ }
4395
+ }
4396
+
4397
+ async function findExistingDataView(config, target, code) {
4398
+ const stateId = target.bound.resources?.dataViews?.[code]?.dataViewId;
4399
+ const byCode = await requestOptionalWithAuth(
4400
+ config,
4401
+ target.profileName,
4402
+ `/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/data-views/${encodeURIComponent(code)}`
4403
+ );
4404
+ if (byCode?.id) return byCode;
4405
+ if (stateId) {
4406
+ return { id: stateId, code };
4407
+ }
4408
+ return null;
4409
+ }
4410
+
4240
4411
  async function findExistingAutomation(config, target, code) {
4241
4412
  const stateId = target.bound.resources?.automations?.[code]?.automationId;
4242
4413
  if (stateId) {
@@ -4369,6 +4540,7 @@ async function pruneResourceManifest(config, target, manifest, result) {
4369
4540
  await pruneMenus(config, target, desiredCodes(manifest.menus), result);
4370
4541
  await pruneWorkflows(config, target, desiredCodes(manifest.workflows), result);
4371
4542
  await pruneAutomations(config, target, desiredCodes(manifest.automations), result);
4543
+ await pruneDataViews(config, target, desiredCodes(manifest.dataViews), result);
4372
4544
  await prunePagePermissionGroups(
4373
4545
  config,
4374
4546
  target,
@@ -4517,6 +4689,29 @@ async function pruneAutomations(config, target, desired, result) {
4517
4689
  }
4518
4690
  }
4519
4691
 
4692
+ async function pruneDataViews(config, target, desired, result) {
4693
+ const data = await requestWithAuth(
4694
+ config,
4695
+ target.profileName,
4696
+ apiPathWithQuery(`/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/data-views`, {
4697
+ page: 1,
4698
+ pageSize: 1000,
4699
+ })
4700
+ );
4701
+ for (const dataView of normalizeItems(data)) {
4702
+ const code = dataView.code || dataView.resourceCode;
4703
+ if (!code || desired.has(code)) continue;
4704
+ await pruneOne(config, target, result, 'dataView', code, dataView.id, async () => {
4705
+ await requestWithAuth(
4706
+ config,
4707
+ target.profileName,
4708
+ `/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/data-views/${encodeURIComponent(code)}`,
4709
+ { method: 'DELETE' }
4710
+ );
4711
+ });
4712
+ }
4713
+ }
4714
+
4520
4715
  async function prunePagePermissionGroups(config, target, desired, result) {
4521
4716
  const data = await requestWithAuth(
4522
4717
  config,
@@ -4591,6 +4786,7 @@ function removeStateResource(target, kind, code) {
4591
4786
  menu: 'menus',
4592
4787
  workflow: 'workflows',
4593
4788
  automation: 'automations',
4789
+ dataView: 'dataViews',
4594
4790
  pagePermissionGroup: 'pagePermissionGroups',
4595
4791
  formPermissionGroup: 'formPermissionGroups',
4596
4792
  };
@@ -4759,6 +4955,31 @@ async function pullResources(config, target) {
4759
4955
  written.push(path.relative(process.cwd(), filePath));
4760
4956
  }
4761
4957
 
4958
+ const dataViews = await requestWithAuth(
4959
+ config,
4960
+ target.profileName,
4961
+ apiPathWithQuery(`/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/data-views`, {
4962
+ page: 1,
4963
+ pageSize: 1000,
4964
+ })
4965
+ );
4966
+ for (const dataView of normalizeItems(dataViews)) {
4967
+ const code = dataView.code || dataView.resourceCode || dataView.id;
4968
+ const detail = await requestWithAuth(
4969
+ config,
4970
+ target.profileName,
4971
+ `/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/data-views/${encodeURIComponent(code)}`
4972
+ );
4973
+ const permissionGroups = await requestOptionalWithAuth(
4974
+ config,
4975
+ target.profileName,
4976
+ `/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/data-views/${encodeURIComponent(code)}/permission-groups`
4977
+ );
4978
+ const filePath = path.join(baseDir, 'data-views', `${code}.json`);
4979
+ writeResourceJsonFile(filePath, toPulledDataView(detail, permissionGroups, pullLookups));
4980
+ written.push(path.relative(process.cwd(), filePath));
4981
+ }
4982
+
4762
4983
  const pageGroups = await requestWithAuth(
4763
4984
  config,
4764
4985
  target.profileName,
@@ -4920,6 +5141,43 @@ function toPulledNotificationTypeConfig(configItem, lookups) {
4920
5141
  });
4921
5142
  }
4922
5143
 
5144
+ function toPulledDataView(dataView, permissionGroups, lookups) {
5145
+ const definition = rewriteDataViewDefinitionForManifest(
5146
+ clonePlainJson(dataView.definition || {}),
5147
+ lookups
5148
+ );
5149
+ const groups = normalizeItems(permissionGroups).map(group => {
5150
+ const item = stripPulledResource(group);
5151
+ delete item.dataViewCode;
5152
+ return item;
5153
+ });
5154
+ return stripUndefinedValues({
5155
+ code: dataView.code || dataView.resourceCode,
5156
+ name: dataView.name,
5157
+ description: dataView.description || '',
5158
+ definition,
5159
+ refreshConfig: dataView.refreshConfig || undefined,
5160
+ permissionGroups: groups.length > 0 ? groups : undefined,
5161
+ });
5162
+ }
5163
+
5164
+ function rewriteDataViewDefinitionForManifest(definition, lookups) {
5165
+ rewriteDataViewSourceForManifest(definition.base, lookups);
5166
+ for (const join of definition.joins || []) {
5167
+ rewriteDataViewSourceForManifest(join, lookups);
5168
+ }
5169
+ return definition;
5170
+ }
5171
+
5172
+ function rewriteDataViewSourceForManifest(source, lookups) {
5173
+ if (!source || typeof source !== 'object') return;
5174
+ const formCode = lookups.formCodeByUuid.get(source.formUuid);
5175
+ if (formCode) {
5176
+ source.formCode = formCode;
5177
+ delete source.formUuid;
5178
+ }
5179
+ }
5180
+
4923
5181
  function stripNotificationSecrets(value) {
4924
5182
  if (!value || typeof value !== 'object') return value;
4925
5183
  if (Array.isArray(value)) return value.map(stripNotificationSecrets);
@@ -5056,6 +5314,70 @@ function notificationTypeConfigExistingKey(config) {
5056
5314
  return `${level}:${config.formUuid || ''}:${config.notificationType}`;
5057
5315
  }
5058
5316
 
5317
+ function normalizeDataViewManifest(bound, dataView) {
5318
+ const definition = dataView.definition !== undefined
5319
+ ? clonePlainJson(dataView.definition)
5320
+ : clonePlainJson(withoutDataViewManifestMeta(dataView));
5321
+ if (!definition.code) definition.code = dataView.code || dataView.resourceCode;
5322
+ if (!definition.name && dataView.name) definition.name = dataView.name;
5323
+ if (definition.description === undefined && dataView.description !== undefined) {
5324
+ definition.description = dataView.description;
5325
+ }
5326
+ const normalizedDefinition = normalizeDataViewDefinitionFormRefs(bound, definition, dataView);
5327
+ return stripUndefinedValues({
5328
+ code: dataView.code || dataView.resourceCode || normalizedDefinition.code,
5329
+ name: dataView.name || normalizedDefinition.name || dataView.code || normalizedDefinition.code,
5330
+ description: dataView.description !== undefined
5331
+ ? dataView.description
5332
+ : normalizedDefinition.description || '',
5333
+ definition: normalizedDefinition,
5334
+ refreshConfig: dataView.refreshConfig || dataView.refresh || normalizedDefinition.refresh,
5335
+ permissionGroups: Array.isArray(dataView.permissionGroups)
5336
+ ? dataView.permissionGroups.map(group => stripUndefinedValues(withoutResourceMeta(group)))
5337
+ : undefined,
5338
+ });
5339
+ }
5340
+
5341
+ function withoutDataViewManifestMeta(dataView) {
5342
+ const next = withoutResourceMeta(dataView);
5343
+ delete next.permissionGroups;
5344
+ delete next.refreshConfig;
5345
+ return next;
5346
+ }
5347
+
5348
+ function normalizeDataViewDefinitionFormRefs(bound, definition, dataView) {
5349
+ if (!definition?.base) {
5350
+ fail(`${resourceLabel('dataView', dataView)} 缺少 base`);
5351
+ }
5352
+ resolveDataViewSourceFormUuid(bound, definition.base, dataView, 'base');
5353
+ for (const [index, join] of (definition.joins || []).entries()) {
5354
+ resolveDataViewSourceFormUuid(bound, join, dataView, `joins[${index}]`);
5355
+ }
5356
+ return definition;
5357
+ }
5358
+
5359
+ function resolveDataViewSourceFormUuid(bound, source, dataView, label) {
5360
+ if (!source || typeof source !== 'object') {
5361
+ fail(`${resourceLabel('dataView', dataView)} ${label} 必须是 object`);
5362
+ }
5363
+ if (source.formUuid) {
5364
+ delete source.formCode;
5365
+ delete source.form;
5366
+ return;
5367
+ }
5368
+ const formCode = source.formCode || source.form;
5369
+ if (!formCode) {
5370
+ fail(`${resourceLabel('dataView', dataView)} ${label} 缺少 formCode 或 formUuid`);
5371
+ }
5372
+ const formUuid = bound.resources?.forms?.[formCode]?.formUuid;
5373
+ if (!formUuid) {
5374
+ fail(`${resourceLabel('dataView', dataView)} ${label}.formCode 未绑定: ${formCode}`);
5375
+ }
5376
+ source.formUuid = formUuid;
5377
+ delete source.formCode;
5378
+ delete source.form;
5379
+ }
5380
+
5059
5381
  async function resolveManifestJson(config, profileName, item, objectKey, fileKey, optional = false) {
5060
5382
  let value = item[objectKey];
5061
5383
  if (value === undefined && item[fileKey]) {
@@ -5315,6 +5637,11 @@ function stripUndefinedValues(value) {
5315
5637
  }, {});
5316
5638
  }
5317
5639
 
5640
+ function clonePlainJson(value) {
5641
+ if (value === undefined) return undefined;
5642
+ return JSON.parse(JSON.stringify(value));
5643
+ }
5644
+
5318
5645
  function workflowEquals(bound, desired, existing) {
5319
5646
  if (!existing) return false;
5320
5647
  const desiredDefinition = resolveManifestPlainJson(desired, 'definitionJson', 'definitionFile');
@@ -5361,6 +5688,57 @@ function automationEquals(target, desired, existing) {
5361
5688
  );
5362
5689
  }
5363
5690
 
5691
+ function dataViewEquals(bound, desired, existing) {
5692
+ if (!existing) return false;
5693
+ const expected = normalizeDataViewManifest(bound, desired);
5694
+ const permissionGroupsEqual = expected.permissionGroups === undefined
5695
+ ? true
5696
+ : dataViewPermissionGroupsEqual(expected.permissionGroups, existing.permissionGroups || []);
5697
+ return (
5698
+ String(existing.code || existing.resourceCode || '') === String(expected.code || '') &&
5699
+ String(existing.name || '') === String(expected.name || '') &&
5700
+ String(existing.description || '') === String(expected.description || '') &&
5701
+ jsonEqualsForPlan(existing.definition || {}, expected.definition || {}, desired.__dir) &&
5702
+ jsonEqualsForPlan(existing.refreshConfig || {}, expected.refreshConfig || {}, desired.__dir) &&
5703
+ permissionGroupsEqual
5704
+ );
5705
+ }
5706
+
5707
+ function dataViewPermissionGroupsEqual(desiredGroups = [], existingGroups = []) {
5708
+ return stableStringifyForPlan(normalizeDataViewPermissionGroupsForPlan(existingGroups), process.cwd()) ===
5709
+ stableStringifyForPlan(normalizeDataViewPermissionGroupsForPlan(desiredGroups), process.cwd());
5710
+ }
5711
+
5712
+ function normalizeDataViewPermissionGroupsForPlan(groups = []) {
5713
+ return (groups || [])
5714
+ .map(group => {
5715
+ const code = group.code || group.resourceCode || '';
5716
+ return stripUndefinedValues({
5717
+ code,
5718
+ name: group.name || code || '默认权限组',
5719
+ roles: Array.isArray(group.roles) ? sortStringValues(group.roles) : [],
5720
+ operations: Array.isArray(group.operations) ? sortStringValues(group.operations) : ['query'],
5721
+ fieldPermissions: normalizeFieldPermissionsForPlan(group.fieldPermissions),
5722
+ dataPermission: group.dataPermission || undefined,
5723
+ });
5724
+ })
5725
+ .sort((left, right) => {
5726
+ const leftKey = `${left.code || ''}:${left.name || ''}`;
5727
+ const rightKey = `${right.code || ''}:${right.name || ''}`;
5728
+ return leftKey.localeCompare(rightKey);
5729
+ });
5730
+ }
5731
+
5732
+ function normalizeFieldPermissionsForPlan(fieldPermissions) {
5733
+ if (!Array.isArray(fieldPermissions) || !fieldPermissions.length) return undefined;
5734
+ return fieldPermissions
5735
+ .map(item => ({
5736
+ field: item.field,
5737
+ value: item.value,
5738
+ }))
5739
+ .sort((left, right) => `${left.field || ''}:${left.value || ''}`.localeCompare(`${right.field || ''}:${right.value || ''}`));
5740
+ }
5741
+
5364
5742
  function resolveManifestPlainJson(item, objectKey, fileKey, optional = false) {
5365
5743
  if (item[objectKey] !== undefined) return item[objectKey];
5366
5744
  if (item[fileKey]) {
@@ -5510,6 +5888,37 @@ function readJsonArg(value, label) {
5510
5888
  return readJsonArgWithBase(value, label).data;
5511
5889
  }
5512
5890
 
5891
+ function buildDataViewQueryBody(flags = {}) {
5892
+ const explicit = flags['query-json'] || flags['params-json'] || flags['body-json'];
5893
+ if (explicit) return readJsonArg(explicit, explicit === flags['query-json'] ? 'query-json' : 'params-json');
5894
+ const body = {};
5895
+ const fields = splitList(flags.fields || flags.field);
5896
+ if (fields.length > 0) body.fields = fields;
5897
+ if (flags['filters-json']) body.filters = readJsonArg(flags['filters-json'], 'filters-json');
5898
+ if (flags['filter-json']) body.filters = readJsonArg(flags['filter-json'], 'filter-json');
5899
+ if (flags['condition-type']) body.conditionType = String(flags['condition-type']).toUpperCase();
5900
+ if (flags.keyword || flags.search) body.searchKeyWord = flags.keyword || flags.search;
5901
+ if (flags.page) body.currentPage = Number(flags.page);
5902
+ if (flags['page-size'] || flags.limit) body.pageSize = Number(flags['page-size'] || flags.limit);
5903
+ if (flags['order-json']) {
5904
+ body.order = readJsonArg(flags['order-json'], 'order-json');
5905
+ } else if (flags['sort-field'] || flags.sort) {
5906
+ const field = flags['sort-field'] || flags.sort;
5907
+ body.order = [
5908
+ {
5909
+ field,
5910
+ isAsc:
5911
+ String(flags.desc || flags.direction || '').toLowerCase() === 'desc'
5912
+ ? 'n'
5913
+ : flags.asc === false
5914
+ ? 'n'
5915
+ : 'y',
5916
+ },
5917
+ ];
5918
+ }
5919
+ return body;
5920
+ }
5921
+
5513
5922
  function readJsonArgWithBase(value, label) {
5514
5923
  if (!value) fail(`缺少 ${label}`);
5515
5924
  const raw = String(value).trim();
@@ -42,6 +42,7 @@ function initWorkspace(options = {}) {
42
42
  menus: {},
43
43
  roles: {},
44
44
  connectors: {},
45
+ dataViews: {},
45
46
  notifications: {
46
47
  templates: {},
47
48
  typeConfigs: {},
@@ -85,12 +85,13 @@ When the user provides a root domain such as `https://yida.wisejob.cn/`, use it
85
85
  - Shared workspace env values such as `APP_OSS_*` should live in `~/.openxiangda/.env` by default. Project `.env` files are only per-workspace overrides.
86
86
  - For suspected platform defects, bugs, or product optimization requests, ask the user to confirm first, then use `openxiangda feedback preview` and `openxiangda feedback submit --yes` to send a detailed DingTalk robot report. Include the command, error, relevant files, and logs/context files when available. The CLI redacts tokens, cookies, secrets, phone numbers, and emails before sending.
87
87
  - For form pages, every visible field should have a concise user-facing `placeholder`. Use `tips` only for special constraints or non-obvious business rules; do not add tips to every field.
88
- - Form pages should display only fields the user needs to see. Use `SelectField` / `RadioField` for enums and `AssociationFormField` for data maintained by another form, such as class, college, customer, or project. Do not make users maintain raw ID text fields.
89
- - Permission scope keys, computed fields, sync fields, and developer/internal fields should be present only when needed for permissions or logic, and should normally be derived from a visible select/association field and marked `behavior: "HIDDEN"`.
88
+ - Form pages should display only fields the user needs to see. Use `SelectField` / `RadioField` for enums. For data maintained by another form, such as class, college, customer, or project, use `SelectField` with `optionSource.type: "linkedForm"` so the runtime queries form data through the SDK and builds dropdown options; set `remoteSearch: true` and an explicit `searchFieldId` when the source data can be large. Do not make users maintain raw ID text fields, and do not use `AssociationFormField` for new form work.
89
+ - Permission scope keys, computed fields, sync fields, and developer/internal fields should be present only when needed for permissions or logic, and should normally be derived from visible select/person/department fields and marked `behavior: "HIDDEN"`. For select-derived scalar keys, use `valueSync`.
90
90
  - Do not add extra fields for platform system metadata such as creator, updater, creator department, updater department, created time, or updated time unless the user explicitly needs a separate business field; the platform already creates system fields for each form.
91
91
  - All visible copy in forms and pages must be written for end users. Do not put implementation notes, developer explanations, schema descriptions, or "this area is generated by..." text into page sections, cards, labels, tips, or empty states.
92
92
  - Use logical resource codes in local files. Platform-specific IDs such as `formUuid`, `pageId`, `workflowId`, and `automationId` must be isolated by profile.
93
93
  - Put engineering-managed resources in `src/resources/` and use `openxiangda resource validate|plan|publish|pull`. `workspace publish` publishes workspace forms/pages first, then runs non-destructive resource upsert. Only pass `--prune` when the user explicitly wants local manifests to delete platform-side extras.
94
+ - For repeated read-only multi-form queries, create a data view manifest in `src/resources/data-views/` and query it with `sdk.dataView.query` or `sdk.dataSource.run`. Use data views for joined list/report/lookup sources where refresh lag is acceptable; do not use them for single-form CRUD, simple linkedForm selects, real-time writes, or write-back. Read `references/data-views.md` before designing one.
94
95
  - For external APIs, create a connector manifest in `src/resources/connectors/` and call it from pages through `sdk.connector`; never put third-party API keys in page source.
95
96
  - Before scaffolding a real app, complex page, data management page, portal shell, status lifecycle, role governance, or automation, read `references/best-practices.md` and pick the architecture first. Prefer copying the relevant pattern from `examples/best-practices/` into `src/` instead of generating a single large page file.
96
97
  - Before publishing to another platform, verify the workspace is bound for that profile. Resource IDs from one profile must not be reused for another profile.
@@ -127,6 +128,7 @@ Core CLI / state:
127
128
  - `references/openxiangda-api.md` — `/openxiangda-api/v1` request and response fields.
128
129
  - `references/workspace-state.md` — `.openxiangda/state.json` shape and profile isolation rules.
129
130
  - `references/connector-resources.md` — `src/resources` manifests, connector schema, and SDK connector calls.
131
+ - `references/data-views.md` — `src/resources/data-views` materialized view resources, DSL, permissions, refresh, CLI commands, and runtime SDK usage.
130
132
  - `references/notifications.md` — `src/resources/notifications`, notification templates/type bindings, and `sdk.notification` / `ctx.notification`.
131
133
  - `references/best-practices.md` — initialized examples for modular pages, state lifecycles, role governance, permission isolation, high-performance queries, portal shells, workflow boundaries, and automation patterns.
132
134
 
@@ -151,7 +153,7 @@ Workflow / automation / permissions:
151
153
  Platform domain knowledge (read these first when generating forms or pages so the output matches the live platform behavior):
152
154
 
153
155
  - `references/platform-data-model.md` — how the platform persists field values (JSONB, `{label, value}` for option fields, attachment shape, etc.). Use this whenever you write, render, or diagnose form data.
154
- - `references/style-system.md` — three-layer style architecture, CSS namespace, and flexible Tailwind/CSS guidance. Use this before writing substantial page or form CSS.
156
+ - `references/style-system.md` — style isolation defaults, legacy namespace compatibility, and flexible Tailwind/CSS guidance. Use this before writing substantial page or form CSS.
155
157
  - `references/architecture-patterns.md` — CRUD data flow, `DataManagementList` pattern, recommended directory layout for `src/pages` and `src/forms`. Use this before scaffolding a new page or list view.
156
158
  - `references/best-practices.md` — read this before implementing app-level business patterns; it points to `examples/best-practices/` and explains when to use status fields instead of workflow.
157
159
  - `references/component-guide.md` — when to pick platform components vs. raw Ant Design vs. custom components, with the rules for option fields. Use this before introducing a new component.