@svadmin/core 0.61.0 → 0.61.1

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@svadmin/core",
3
- "version": "0.61.0",
3
+ "version": "0.61.1",
4
4
  "author": "zuohuadong",
5
5
  "repository": {
6
6
  "type": "git",
@@ -27,8 +27,16 @@ export function buildProductPagePlan(surface: ProductSurface, plan: ProductPlan)
27
27
  .filter(id => id !== surface.primaryActionId)
28
28
  .map(id => actionById.get(id))
29
29
  .filter((action): action is ProductAction => action !== undefined);
30
+ // ProductSurface currently declares section titles separately from its
31
+ // resource list. Keep resource declarations in one section until the plan
32
+ // schema can express per-section ownership; repeating every resource in
33
+ // every section makes generated pages look like duplicate data.
30
34
  const sections = surface.sections.length
31
- ? surface.sections.map((title, index) => ({ id: `${surface.id}.section.${index + 1}`, title, resourceIds: surface.resourceIds }))
35
+ ? surface.sections.map((title, index) => ({
36
+ id: `${surface.id}.section.${index + 1}`,
37
+ title,
38
+ resourceIds: index === 0 ? surface.resourceIds : [],
39
+ }))
32
40
  : [{ id: `${surface.id}.content`, title: surface.name, resourceIds: surface.resourceIds }];
33
41
  return {
34
42
  id: surface.id,
@@ -37,6 +37,40 @@ export function reviewProductPlan(plan: ProductPlan): ProductReviewReport {
37
37
  const surfaceIds = new Set(plan.surfaces.map(surface => surface.id));
38
38
  const actionIds = new Set(plan.actions.map(action => action.id));
39
39
 
40
+ const runtimeNames = new Map<string, string>();
41
+ for (const resource of plan.resources) {
42
+ const runtimeName = resource.runtimeName ?? resource.name;
43
+ const previous = runtimeNames.get(runtimeName);
44
+ if (previous !== undefined) {
45
+ addIssue(issues, {
46
+ code: 'resource.duplicate-runtime-name',
47
+ severity: 'error',
48
+ message: `Resources "${resource.label}" and "${previous}" resolve to the same runtime name "${runtimeName}".`,
49
+ path: `resources.${resource.id}.runtimeName`,
50
+ suggestion: 'Give every generated resource route a unique runtime name.',
51
+ });
52
+ } else {
53
+ runtimeNames.set(runtimeName, resource.label);
54
+ }
55
+ }
56
+
57
+ const surfaceRoutes = new Map<string, string>();
58
+ for (const surface of plan.surfaces) {
59
+ const route = surface.route ?? `/workflows/${surface.id}`;
60
+ const previous = surfaceRoutes.get(route);
61
+ if (previous !== undefined) {
62
+ addIssue(issues, {
63
+ code: 'surface.duplicate-route',
64
+ severity: 'error',
65
+ message: `Surfaces "${surface.name}" and "${previous}" resolve to the same route "${route}".`,
66
+ path: `surfaces.${surface.id}.route`,
67
+ suggestion: 'Give every generated surface a unique route.',
68
+ });
69
+ } else {
70
+ surfaceRoutes.set(route, surface.name);
71
+ }
72
+ }
73
+
40
74
  for (const workflow of plan.workflows) {
41
75
  if (!workflow.actorRoleIds.length) addIssue(issues, { code: 'workflow.missing-actor', severity: 'error', message: `Workflow "${workflow.name}" has no actor role.`, path: `workflows.${workflow.id}.actorRoleIds` });
42
76
  for (const roleId of workflow.actorRoleIds) if (!has(roleIds, roleId)) addIssue(issues, { code: 'workflow.unknown-role', severity: 'error', message: `Workflow "${workflow.name}" references unknown role "${roleId}".`, path: `workflows.${workflow.id}.actorRoleIds` });
@@ -46,12 +80,34 @@ export function reviewProductPlan(plan: ProductPlan): ProductReviewReport {
46
80
  for (const resource of plan.resources) {
47
81
  if (!resource.workflowIds.length && !resource.internal) addIssue(issues, { code: 'resource.no-workflow', severity: 'warning', message: `Resource "${resource.label}" is not attached to a workflow.`, path: `resources.${resource.id}.workflowIds`, suggestion: 'Make it contextual/internal or attach it to a real task.' });
48
82
  for (const workflowId of resource.workflowIds) if (!has(workflowIds, workflowId)) addIssue(issues, { code: 'resource.unknown-workflow', severity: 'error', message: `Resource "${resource.label}" references unknown workflow "${workflowId}".`, path: `resources.${resource.id}.workflowIds` });
83
+ for (const roleId of resource.roleIds) if (!has(roleIds, roleId)) addIssue(issues, { code: 'resource.unknown-role', severity: 'error', message: `Resource "${resource.label}" references unknown role "${roleId}".`, path: `resources.${resource.id}.roleIds` });
84
+ resource.fields.forEach(field => {
85
+ for (const roleId of field.readableBy) if (!has(roleIds, roleId)) addIssue(issues, { code: 'field.unknown-role', severity: 'error', message: `Field "${field.label}" references unknown readable role "${roleId}".`, path: `resources.${resource.id}.fields.${field.id}.readableBy` });
86
+ for (const roleId of field.writableBy) if (!has(roleIds, roleId)) addIssue(issues, { code: 'field.unknown-role', severity: 'error', message: `Field "${field.label}" references unknown writable role "${roleId}".`, path: `resources.${resource.id}.fields.${field.id}.writableBy` });
87
+ for (const workflowId of field.usedByWorkflowIds) if (!has(workflowIds, workflowId)) addIssue(issues, { code: 'field.unknown-workflow', severity: 'error', message: `Field "${field.label}" references unknown workflow "${workflowId}".`, path: `resources.${resource.id}.fields.${field.id}.usedByWorkflowIds` });
88
+ });
49
89
  }
50
90
 
51
91
  for (const surface of plan.surfaces) {
52
- reviewSurface(surface, { workflowIds, actionIds, resourceIds }, issues);
92
+ reviewSurface(surface, { workflowIds, actionIds, resourceIds, roleIds, surfaceIds }, issues);
53
93
  }
54
94
 
95
+ const visitingSurfaces = new Set<string>();
96
+ const visitedSurfaces = new Set<string>();
97
+ const visitSurface = (id: string): void => {
98
+ if (visitingSurfaces.has(id)) {
99
+ addIssue(issues, { code: 'surface.cycle', severity: 'error', message: `Surface hierarchy contains a cycle at "${id}".`, path: `surfaces.${id}.parentSurfaceId` });
100
+ return;
101
+ }
102
+ if (visitedSurfaces.has(id)) return;
103
+ visitingSurfaces.add(id);
104
+ const parentId = plan.surfaces.find(surface => surface.id === id)?.parentSurfaceId;
105
+ if (parentId !== undefined && surfaceIds.has(parentId)) visitSurface(parentId);
106
+ visitingSurfaces.delete(id);
107
+ visitedSurfaces.add(id);
108
+ };
109
+ for (const surface of plan.surfaces) visitSurface(surface.id);
110
+
55
111
  const menuById = new Map(plan.menu.map(item => [item.id, item] as const));
56
112
  for (const item of plan.menu) {
57
113
  if (item.parentId !== undefined && !menuById.has(item.parentId)) {
@@ -78,11 +134,14 @@ export function reviewProductPlan(plan: ProductPlan): ProductReviewReport {
78
134
  if (!item.workflowIds.length && !item.resourceId && !item.surfaceId) addIssue(issues, { code: 'menu.no-business-target', severity: 'warning', message: `Menu item "${item.label}" has no workflow, resource or surface target.`, path: `menu.${item.id}`, suggestion: 'Keep it only for an intentional external/system entry.' });
79
135
  if (item.surfaceId && !has(surfaceIds, item.surfaceId)) addIssue(issues, { code: 'menu.unknown-surface', severity: 'error', message: `Menu item "${item.label}" references unknown surface.`, path: `menu.${item.id}.surfaceId` });
80
136
  if (item.resourceId && !has(resourceIds, item.resourceId)) addIssue(issues, { code: 'menu.unknown-resource', severity: 'error', message: `Menu item "${item.label}" references unknown resource.`, path: `menu.${item.id}.resourceId` });
137
+ for (const workflowId of item.workflowIds) if (!has(workflowIds, workflowId)) addIssue(issues, { code: 'menu.unknown-workflow', severity: 'error', message: `Menu item "${item.label}" references unknown workflow "${workflowId}".`, path: `menu.${item.id}.workflowIds` });
138
+ for (const roleId of item.roleIds) if (!has(roleIds, roleId)) addIssue(issues, { code: 'menu.unknown-role', severity: 'error', message: `Menu item "${item.label}" references unknown role "${roleId}".`, path: `menu.${item.id}.roleIds` });
81
139
  if (!item.roleIds.length && item.placement !== 'hidden') addIssue(issues, { code: 'menu.public-entry', severity: 'info', message: `Menu item "${item.label}" is public at the UI hint layer.`, path: `menu.${item.id}.roleIds`, suggestion: 'Keep server authorization independent from menu visibility.' });
82
140
  }
83
141
 
84
142
  for (const action of plan.actions) {
85
143
  if (!has(workflowIds, action.workflowId)) addIssue(issues, { code: 'action.unknown-workflow', severity: 'error', message: `Action "${action.label}" references unknown workflow.`, path: `actions.${action.id}.workflowId` });
144
+ for (const roleId of action.roleIds) if (!has(roleIds, roleId)) addIssue(issues, { code: 'action.unknown-role', severity: 'error', message: `Action "${action.label}" references unknown role "${roleId}".`, path: `actions.${action.id}.roleIds` });
86
145
  if (!action.roleIds.length) addIssue(issues, { code: 'action.missing-roles', severity: 'warning', message: `Action "${action.label}" has no role visibility contract.`, path: `actions.${action.id}.roleIds` });
87
146
  }
88
147
 
@@ -110,8 +169,10 @@ export function reviewProductPlan(plan: ProductPlan): ProductReviewReport {
110
169
  };
111
170
  }
112
171
 
113
- function reviewSurface(surface: ProductSurface, refs: { workflowIds: ReadonlySet<string>; actionIds: ReadonlySet<string>; resourceIds: ReadonlySet<string> }, issues: ProductReviewIssue[]): void {
172
+ function reviewSurface(surface: ProductSurface, refs: { workflowIds: ReadonlySet<string>; actionIds: ReadonlySet<string>; resourceIds: ReadonlySet<string>; roleIds: ReadonlySet<string>; surfaceIds: ReadonlySet<string> }, issues: ProductReviewIssue[]): void {
114
173
  if (!surface.purpose.trim()) addIssue(issues, { code: 'surface.missing-purpose', severity: 'error', message: `Surface "${surface.name}" has no purpose.`, path: `surfaces.${surface.id}.purpose` });
174
+ for (const roleId of surface.roleIds) if (!refs.roleIds.has(roleId)) addIssue(issues, { code: 'surface.unknown-role', severity: 'error', message: `Surface "${surface.name}" references unknown role "${roleId}".`, path: `surfaces.${surface.id}.roleIds` });
175
+ if (surface.parentSurfaceId !== undefined && !refs.surfaceIds.has(surface.parentSurfaceId)) addIssue(issues, { code: 'surface.unknown-parent', severity: 'error', message: `Surface "${surface.name}" references unknown parent surface "${surface.parentSurfaceId}".`, path: `surfaces.${surface.id}.parentSurfaceId` });
115
176
  if (!surface.workflowIds.length) addIssue(issues, { code: 'surface.no-workflow', severity: 'warning', message: `Surface "${surface.name}" is not attached to a workflow.`, path: `surfaces.${surface.id}.workflowIds` });
116
177
  surface.workflowIds.forEach(id => { if (!refs.workflowIds.has(id)) addIssue(issues, { code: 'surface.unknown-workflow', severity: 'error', message: `Surface "${surface.name}" references unknown workflow "${id}".`, path: `surfaces.${surface.id}.workflowIds` }); });
117
178
  surface.resourceIds.forEach(id => { if (!refs.resourceIds.has(id)) addIssue(issues, { code: 'surface.unknown-resource', severity: 'error', message: `Surface "${surface.name}" references unknown resource "${id}".`, path: `surfaces.${surface.id}.resourceIds` }); });