openxiangda 1.0.230 → 1.0.231

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 (2) hide show
  1. package/lib/cli.js +234 -39
  2. package/package.json +1 -1
package/lib/cli.js CHANGED
@@ -16309,7 +16309,12 @@ function removeDirectStateResource(target, resourceType, code) {
16309
16309
  function normalizeMenuDirectBody(bound, menuItem) {
16310
16310
  const formUuid = resolveManifestFormUuid(bound, menuItem);
16311
16311
  const pageId = resolveManifestPageId(bound, menuItem);
16312
- const parentId = resolveManifestMenuId(bound, menuItem.parentCode) || menuItem.parentId || null;
16312
+ const parentId = requireResolvedMenuParentId(
16313
+ bound,
16314
+ menuItem,
16315
+ new Map(),
16316
+ 'direct menu mutation'
16317
+ );
16313
16318
  return stripUndefinedValues({
16314
16319
  resourceCode: menuItem.code || menuItem.resourceCode,
16315
16320
  name: menuItem.name || menuItem.code,
@@ -19087,7 +19092,19 @@ async function buildResourcePlan(
19087
19092
  ]);
19088
19093
  const actions = [];
19089
19094
  addPlanActions(actions, 'role', manifest.roles, existing.roles, roleEquals);
19090
- addPlanActions(actions, 'menu', manifest.menus, existing.menus, (item, current) => menuEquals(target.bound, item, current));
19095
+ const orderedMenus = orderMenuResourcesForPublish(
19096
+ manifest.menus,
19097
+ target.bound,
19098
+ existing.menus
19099
+ );
19100
+ addPlanActions(
19101
+ actions,
19102
+ 'menu',
19103
+ orderedMenus,
19104
+ existing.menus,
19105
+ (item, current) =>
19106
+ menuEquals(target.bound, item, current, existing.menus)
19107
+ );
19091
19108
  addPlanActions(actions, 'connector', manifest.connectors, existing.connectors, connectorEquals);
19092
19109
  addNotificationPlanActions(target, actions, manifest.notifications || [], existing);
19093
19110
  await addWorkflowPlanActions(config, target, actions, manifest.workflows, existing.workflows);
@@ -22594,21 +22611,61 @@ async function publishFormSettingsResources(
22594
22611
  }
22595
22612
 
22596
22613
  async function publishMenuResources(config, target, menus, result, options = {}) {
22597
- for (const menuItem of menus) {
22598
- if (shouldSkipNoopResource(options, 'menu', menuItem.code)) {
22599
- recordNoopResource(result, 'menu', menuItem.code, options);
22614
+ if (menus.length === 0) return;
22615
+ const existingMenus = await fetchExistingMenusByCode(config, target);
22616
+ const orderedMenus = orderMenuResourcesForPublish(
22617
+ menus,
22618
+ target.bound,
22619
+ existingMenus
22620
+ );
22621
+ for (const menuItem of orderedMenus) {
22622
+ const code = menuResourceCode(menuItem);
22623
+ const plannedAction = getPlannedResourceAction(options, 'menu', code);
22624
+ const existing = existingMenus.get(code);
22625
+ if (existing && !menuPlatformId(existing)) {
22626
+ const error = new Error(
22627
+ `MENU_ONLINE_ID_REQUIRED: 在线 menu ${code} 缺少 menuId,禁止执行恢复发布`
22628
+ );
22629
+ error.code = 'MENU_ONLINE_ID_REQUIRED';
22630
+ throw error;
22631
+ }
22632
+ if (
22633
+ ['noop', 'update'].includes(plannedAction?.action) &&
22634
+ !menuPlatformId(existing)
22635
+ ) {
22636
+ const error = new Error(
22637
+ `MENU_PLAN_TARGET_MISSING: menu ${code} 的冻结计划为 ${plannedAction.action},但写入前在线菜单已不存在`
22638
+ );
22639
+ error.code = 'MENU_PLAN_TARGET_MISSING';
22640
+ throw error;
22641
+ }
22642
+ }
22643
+ for (const menuItem of orderedMenus) {
22644
+ const code = menuResourceCode(menuItem);
22645
+ const existing = existingMenus.get(code);
22646
+ if (shouldSkipNoopResource(options, 'menu', code)) {
22647
+ const existingId = menuPlatformId(existing);
22648
+ saveMenuResource(target, code, existingId, {
22649
+ formUuid: existing.formUuid,
22650
+ pageId: existing.pageId,
22651
+ parentId: existing.parentId,
22652
+ routeCode: existing.routeCode,
22653
+ path: existing.path,
22654
+ });
22655
+ recordNoopResource(result, 'menu', code, options);
22600
22656
  continue;
22601
22657
  }
22602
- const existing = await findExistingMenu(config, target, menuItem.code);
22603
22658
  const formUuid = resolveManifestFormUuid(target.bound, menuItem);
22604
22659
  const pageId = resolveManifestPageId(target.bound, menuItem);
22605
- const parentId =
22606
- resolveManifestMenuId(target.bound, menuItem.parentCode) ||
22607
- menuItem.parentId ||
22608
- null;
22660
+ const parentId = requireResolvedMenuParentId(
22661
+ target.bound,
22662
+ menuItem,
22663
+ existingMenus,
22664
+ `resource publish menu ${code}`
22665
+ );
22609
22666
  const body = {
22610
- resourceCode: menuItem.code,
22611
- name: menuItem.name || menuItem.code,
22667
+ resourceCode: code,
22668
+ name: menuItem.name || code,
22612
22669
  type: menuItem.type || 'nav',
22613
22670
  formUuid,
22614
22671
  pageId,
@@ -22619,11 +22676,12 @@ async function publishMenuResources(config, target, menus, result, options = {})
22619
22676
  routeCode: menuItem.routeCode || null,
22620
22677
  path: menuItem.path || null,
22621
22678
  };
22679
+ const existingId = menuPlatformId(existing);
22622
22680
  let data = existing
22623
22681
  ? await requestWithAuth(
22624
22682
  config,
22625
22683
  target.profileName,
22626
- `/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/menus/${encodeURIComponent(existing.id)}`,
22684
+ `/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/menus/${encodeURIComponent(existingId)}`,
22627
22685
  { method: 'POST', body }
22628
22686
  )
22629
22687
  : await requestWithAuth(
@@ -22632,37 +22690,54 @@ async function publishMenuResources(config, target, menus, result, options = {})
22632
22690
  `/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/menus`,
22633
22691
  { method: 'POST', body }
22634
22692
  );
22635
- if (data?.id) {
22636
- saveMenuResource(target, menuItem.code, data.id, {
22637
- resourceCode: menuItem.code,
22638
- name: data.name,
22639
- type: data.type,
22640
- ...(formUuid ? { formUuid } : {}),
22641
- ...(pageId ? { pageId } : {}),
22642
- ...(menuItem.pageCode ? { pageCode: menuItem.pageCode } : {}),
22643
- ...(menuItem.routeCode || data.routeCode
22644
- ? { routeCode: data.routeCode || menuItem.routeCode }
22645
- : {}),
22646
- ...(menuItem.path || data.path ? { path: data.path || menuItem.path } : {}),
22647
- });
22693
+ if (!data?.id) {
22694
+ const error = new Error(
22695
+ `MENU_WRITE_ID_REQUIRED: menu ${code} 写入成功响应缺少 menuId,禁止继续发布子菜单`
22696
+ );
22697
+ error.code = 'MENU_WRITE_ID_REQUIRED';
22698
+ throw error;
22648
22699
  }
22649
- result.published.push({ kind: 'menu', code: menuItem.code, action: existing ? 'update' : 'create', id: data?.id });
22700
+ saveMenuResource(target, code, data.id, {
22701
+ resourceCode: code,
22702
+ name: data.name,
22703
+ type: data.type,
22704
+ ...(formUuid ? { formUuid } : {}),
22705
+ ...(pageId ? { pageId } : {}),
22706
+ ...(menuItem.pageCode ? { pageCode: menuItem.pageCode } : {}),
22707
+ ...(menuItem.routeCode || data.routeCode
22708
+ ? { routeCode: data.routeCode || menuItem.routeCode }
22709
+ : {}),
22710
+ ...(menuItem.path || data.path ? { path: data.path || menuItem.path } : {}),
22711
+ parentId: data.parentId || parentId,
22712
+ });
22713
+ existingMenus.set(code, {
22714
+ ...(existing || {}),
22715
+ ...body,
22716
+ ...data,
22717
+ id: data.id,
22718
+ });
22719
+ result.published.push({
22720
+ kind: 'menu',
22721
+ code,
22722
+ action: existing ? 'update' : 'create',
22723
+ id: data?.id,
22724
+ });
22650
22725
  }
22651
22726
  }
22652
22727
 
22653
- async function findExistingMenu(config, target, code) {
22728
+ async function fetchExistingMenusByCode(config, target) {
22654
22729
  const data = await requestWithAuth(
22655
22730
  config,
22656
22731
  target.profileName,
22657
22732
  `/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/menus`
22658
22733
  );
22659
- const items = flattenItems(normalizeItems(data));
22660
- const stateId = target.bound.resources?.menus?.[code]?.menuId;
22661
- if (stateId) {
22662
- const byState = items.find(item => item.id === stateId);
22663
- if (byState) return byState;
22664
- }
22665
- return items.find(item => item.resourceCode === code);
22734
+ const menus = new Map();
22735
+ indexByCode(
22736
+ menus,
22737
+ flattenItems(normalizeItems(data)),
22738
+ item => item.resourceCode
22739
+ );
22740
+ return menus;
22666
22741
  }
22667
22742
 
22668
22743
  async function publishConnectorResources(config, target, connectors, result, options = {}) {
@@ -26026,7 +26101,123 @@ function resolveManifestPageId(bound, item) {
26026
26101
 
26027
26102
  function resolveManifestMenuId(bound, menuCode) {
26028
26103
  if (!menuCode) return undefined;
26029
- return bound.resources?.menus?.[menuCode]?.menuId || menuCode;
26104
+ return bound.resources?.menus?.[menuCode]?.menuId;
26105
+ }
26106
+
26107
+ function menuResourceCode(menuItem) {
26108
+ return String(menuItem?.code || menuItem?.resourceCode || '').trim();
26109
+ }
26110
+
26111
+ function menuPlatformId(menuItem) {
26112
+ const value = menuItem?.id || menuItem?.menuId;
26113
+ return value ? String(value) : undefined;
26114
+ }
26115
+
26116
+ function resolveMenuParentId(bound, menuItem, existingMenus = new Map()) {
26117
+ const parentCode = String(menuItem?.parentCode || '').trim();
26118
+ if (!parentCode) return menuItem?.parentId || null;
26119
+ const boundId = resolveManifestMenuId(bound, parentCode);
26120
+ const onlineId = menuPlatformId(existingMenus.get(parentCode));
26121
+ return onlineId || boundId || menuItem?.parentId;
26122
+ }
26123
+
26124
+ function menuParentResolutionError(code, parentCode, label) {
26125
+ const error = new Error(
26126
+ `MENU_PARENT_UNRESOLVED: ${label || `menu ${code}`} 无法将 parentCode ${parentCode} 解析为在线 menuId`
26127
+ );
26128
+ error.code = 'MENU_PARENT_UNRESOLVED';
26129
+ return error;
26130
+ }
26131
+
26132
+ function requireResolvedMenuParentId(
26133
+ bound,
26134
+ menuItem,
26135
+ existingMenus = new Map(),
26136
+ label
26137
+ ) {
26138
+ const parentId = resolveMenuParentId(bound, menuItem, existingMenus);
26139
+ if (menuItem?.parentCode && !parentId) {
26140
+ throw menuParentResolutionError(
26141
+ menuResourceCode(menuItem),
26142
+ menuItem.parentCode,
26143
+ label
26144
+ );
26145
+ }
26146
+ return parentId || null;
26147
+ }
26148
+
26149
+ function orderMenuResourcesForPublish(
26150
+ menus = [],
26151
+ bound = {},
26152
+ existingMenus = new Map()
26153
+ ) {
26154
+ const items = Array.isArray(menus) ? menus : [];
26155
+ const byCode = new Map();
26156
+ const sourceIndex = new Map();
26157
+ for (const [index, menuItem] of items.entries()) {
26158
+ const code = menuResourceCode(menuItem);
26159
+ if (!code) {
26160
+ const error = new Error('MENU_CODE_REQUIRED: menu resource 缺少 code');
26161
+ error.code = 'MENU_CODE_REQUIRED';
26162
+ throw error;
26163
+ }
26164
+ if (byCode.has(code)) {
26165
+ const error = new Error(`MENU_CODE_DUPLICATE: menu code ${code} 重复`);
26166
+ error.code = 'MENU_CODE_DUPLICATE';
26167
+ throw error;
26168
+ }
26169
+ byCode.set(code, menuItem);
26170
+ sourceIndex.set(code, index);
26171
+ }
26172
+
26173
+ const children = new Map();
26174
+ const indegree = new Map([...byCode.keys()].map(code => [code, 0]));
26175
+ for (const [code, menuItem] of byCode) {
26176
+ const parentCode = String(menuItem.parentCode || '').trim();
26177
+ if (!parentCode) continue;
26178
+ if (!byCode.has(parentCode)) {
26179
+ if (!resolveMenuParentId(bound, menuItem, existingMenus)) {
26180
+ throw menuParentResolutionError(code, parentCode, `menu ${code}`);
26181
+ }
26182
+ continue;
26183
+ }
26184
+ indegree.set(code, indegree.get(code) + 1);
26185
+ const siblings = children.get(parentCode) || [];
26186
+ siblings.push(code);
26187
+ children.set(parentCode, siblings);
26188
+ }
26189
+
26190
+ const bySourceOrder = (left, right) =>
26191
+ sourceIndex.get(left) - sourceIndex.get(right);
26192
+ const ready = [...indegree.entries()]
26193
+ .filter(([, count]) => count === 0)
26194
+ .map(([code]) => code)
26195
+ .sort(bySourceOrder);
26196
+ const ordered = [];
26197
+ while (ready.length > 0) {
26198
+ const code = ready.shift();
26199
+ ordered.push(byCode.get(code));
26200
+ for (const childCode of (children.get(code) || []).sort(bySourceOrder)) {
26201
+ const next = indegree.get(childCode) - 1;
26202
+ indegree.set(childCode, next);
26203
+ if (next === 0) {
26204
+ ready.push(childCode);
26205
+ ready.sort(bySourceOrder);
26206
+ }
26207
+ }
26208
+ }
26209
+ if (ordered.length !== items.length) {
26210
+ const cycleCodes = [...indegree.entries()]
26211
+ .filter(([, count]) => count > 0)
26212
+ .map(([code]) => code)
26213
+ .sort(bySourceOrder);
26214
+ const error = new Error(
26215
+ `MENU_PARENT_CYCLE: menu parentCode 存在循环依赖: ${cycleCodes.join(' -> ')}`
26216
+ );
26217
+ error.code = 'MENU_PARENT_CYCLE';
26218
+ throw error;
26219
+ }
26220
+ return ordered;
26030
26221
  }
26031
26222
 
26032
26223
  function resolveAutomationManifestPayload(target, automationItem) {
@@ -26178,12 +26369,16 @@ function unwrapFormSettingPlanValue(value, key) {
26178
26369
  return value;
26179
26370
  }
26180
26371
 
26181
- function menuEquals(bound, desired, existing) {
26372
+ function menuEquals(bound, desired, existing, existingMenus = new Map()) {
26182
26373
  if (!existing) return false;
26183
26374
  const desiredFormUuid = resolveManifestFormUuid(bound, desired);
26184
26375
  const desiredPageId = resolveManifestPageId(bound, desired);
26185
- const desiredParentId =
26186
- resolveManifestMenuId(bound, desired.parentCode) || desired.parentId || null;
26376
+ const desiredParentId = resolveMenuParentId(
26377
+ bound,
26378
+ desired,
26379
+ existingMenus
26380
+ );
26381
+ if (desired.parentCode && !desiredParentId) return false;
26187
26382
  return (
26188
26383
  String(existing.name || '') === String(desired.name || desired.code || '') &&
26189
26384
  String(existing.type || '') === String(desired.type || 'nav') &&
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openxiangda",
3
- "version": "1.0.230",
3
+ "version": "1.0.231",
4
4
  "description": "OpenXiangda CLI, workspace build tools, runtime SDK, and form components.",
5
5
  "private": false,
6
6
  "bin": {