refactor: deprecate IUIService, unify into IMetadataService#666
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
- Add auth context parameter to getEffective() signature and implementation - Add UI convenience methods (getView, listViews, getDashboard, listDashboards) to IMetadataService interface and MetadataManager implementation - Mark IUIService as @deprecated with migration guide (will be removed in v4.0.0) - Update DevPlugin UI stub with deprecation notice - Update dispatcher routes, ROADMAP, API plan, llms.txt, and docs - Add tests for UI convenience methods and getEffective with context Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Deprecates the redundant IUIService contract and consolidates UI metadata access into IMetadataService, adding UI convenience methods and extending getEffective() to accept optional auth context for user-scoped overlay resolution. Updates docs/plans/roadmap to reflect the migration.
Changes:
- Mark
IUIServiceas@deprecatedand document migration toIMetadataService. - Add UI convenience methods (
getView,listViews,getDashboard,listDashboards) and extendgetEffective()with optional auth context. - Implement + test the new
IMetadataServicebehaviors inMetadataManager, and update docs/roadmap/dispatcher notes accordingly.
Reviewed changes
Copilot reviewed 10 out of 10 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| packages/spec/src/contracts/ui-service.ts | Adds deprecation JSDoc + migration guidance to IMetadataService. |
| packages/spec/src/contracts/metadata-service.ts | Adds UI convenience method signatures; extends getEffective() signature with auth context. |
| packages/spec/src/api/dispatcher.zod.ts | Annotates /api/v1/ui route as deprecated in favor of metadata routes. |
| packages/spec/llms.txt | Updates contract table to mark IUIService deprecated and point to IMetadataService. |
| packages/spec/API_IMPLEMENTATION_PLAN.md | Replaces plugin-ui-api plan with metadata API equivalents and endpoints. |
| packages/plugins/plugin-dev/src/dev-plugin.ts | Adds _deprecated field to UI dev stub; updates its comment. |
| packages/metadata/src/metadata-service.test.ts | Adds tests for UI convenience methods and getEffective() with userId context. |
| packages/metadata/src/metadata-manager.ts | Implements UI convenience methods; updates overlay resolution in getEffective(). |
| content/docs/guides/contracts/metadata-service.mdx | Adds “UI Metadata” section and overlay/customization examples. |
| ROADMAP.md | Marks IUIService deprecated and updates plan items accordingly. |
| * - getView(name) → metadata.get('view', name) | ||
| * - listViews(object?) → metadata.listViews(object?) | ||
| * - registerView(name, def) → metadata.register('view', name, def) | ||
| * - getDashboard(name) → metadata.get('dashboard', name) | ||
| * - listDashboards() → metadata.listDashboards() | ||
| * - registerDashboard(name, def) → metadata.register('dashboard', name, def) | ||
| * |
There was a problem hiding this comment.
The @deprecated migration examples recommend metadata.listViews(...)/metadata.getView(...), but in the updated IMetadataService contract those methods are optional. To avoid forcing consumers into unsafe calls/casts, consider updating this deprecation JSDoc to recommend the guaranteed APIs (metadata.get('view', name), metadata.list('view'), etc.) and mention the convenience methods only as optional helpers.
| * - getView(name) → metadata.get('view', name) | |
| * - listViews(object?) → metadata.listViews(object?) | |
| * - registerView(name, def) → metadata.register('view', name, def) | |
| * - getDashboard(name) → metadata.get('dashboard', name) | |
| * - listDashboards() → metadata.listDashboards() | |
| * - registerDashboard(name, def) → metadata.register('dashboard', name, def) | |
| * | |
| * | |
| * Recommended migration targets (guaranteed APIs): | |
| * - getView(name) → metadata.get('view', name) | |
| * - listViews(object?) → metadata.list('view', { object }) | |
| * - registerView(name, def) → metadata.register('view', name, def) | |
| * - getDashboard(name) → metadata.get('dashboard', name) | |
| * - listDashboards() → metadata.list('dashboard') | |
| * - registerDashboard(name, def) → metadata.register('dashboard', name, def) | |
| * | |
| * Optional convenience helpers (only if your IMetadataService implementation exposes them): | |
| * - metadata.getView?.(name) | |
| * - metadata.listViews?.(object) | |
| * - metadata.getDashboard?.(name) | |
| * - metadata.listDashboards?.() | |
| * - metadata.registerView?.(name, def) | |
| * - metadata.registerDashboard?.(name, def) | |
| * |
| /** IUIService — delegates to IMetadataService when available, falls back to in-memory Map */ | ||
| function createUIStub() { | ||
| const views = new Map<string, any>(); | ||
| const dashboards = new Map<string, any>(); | ||
| return { | ||
| _dev: true, _serviceName: 'ui', | ||
| _deprecated: 'Use IMetadataService instead. This stub will be removed in v4.0.0.', | ||
| getView(name: string) { return views.get(name); }, |
There was a problem hiding this comment.
The stub’s header comment says it “delegates to IMetadataService when available”, but this implementation is purely an in-memory Map and does not delegate. Either adjust the comment to match behavior, or implement actual delegation (e.g., if the dev kernel has a metadata service, forward getView/listViews/etc. to it).
| ## UI Metadata (Views & Dashboards) | ||
|
|
||
| UI metadata types (`view`, `dashboard`, `page`, `app`, `theme`) are first-class citizens in the Metadata Service. The previously separate `IUIService` has been deprecated. | ||
|
|
||
| ### Reading UI Metadata | ||
|
|
||
| ```typescript | ||
| // Get a view definition | ||
| const view = await metadataService.get('view', 'account_list'); | ||
|
|
||
| // List all views for an object | ||
| const views = await metadataService.listViews('account'); | ||
|
|
||
| // Get a dashboard | ||
| const dashboard = await metadataService.get('dashboard', 'sales_overview'); | ||
| ``` |
There was a problem hiding this comment.
This doc uses metadataService.listViews('account') / getEffective(..., { userId }) as the primary UI examples. Since listViews() is optional in the IMetadataService contract, readers may not be able to call it safely on all implementations. Consider either switching the examples to metadataService.list('view') + filtering, or explicitly stating that listViews()/getView() are convenience helpers provided by MetadataManager (and may not exist on every IMetadataService).
| if (context?.userId) { | ||
| // Try user-specific key first, then fall back to generic user overlay. | ||
| // The owner check below ensures we never apply another user's overlay. | ||
| const userOverlayKey = this.overlayKey(type, name, 'user') + `:${context.userId}`; | ||
| const userOverlay = this.overlays.get(userOverlayKey) | ||
| ?? await this.getOverlay(type, name, 'user'); | ||
| if (userOverlay?.active && userOverlay.patch) { | ||
| // Apply if: overlay has no owner (generic user-level), or owner matches current user | ||
| if (!userOverlay.owner || userOverlay.owner === context.userId) { | ||
| effective = { ...effective, ...userOverlay.patch }; |
There was a problem hiding this comment.
getEffective() tries to load a user-specific overlay via this.overlayKey(..., 'user') + ":${context.userId}", but overlayKey()/saveOverlay() currently key overlays only by type:name:scope (no owner). As a result this lookup can never succeed, and—more importantly—it's impossible to store more than one user overlay per item because user overlays will overwrite each other. Consider incorporating owner into the storage key for scope==='user' (and updating getOverlay/saveOverlay/removeOverlay accordingly, plus a test that saves overlays for two different users and resolves each correctly).
| getView?(name: string): Promise<unknown | undefined>; | ||
|
|
||
| /** | ||
| * Convenience: list view definitions, optionally filtered by object | ||
| */ | ||
| listViews?(object?: string): Promise<unknown[]>; | ||
|
|
||
| /** | ||
| * Convenience: get a dashboard definition by name | ||
| * Equivalent to get('dashboard', name) | ||
| */ | ||
| getDashboard?(name: string): Promise<unknown | undefined>; | ||
|
|
||
| /** | ||
| * Convenience: list all dashboard definitions | ||
| */ | ||
| listDashboards?(): Promise<unknown[]>; |
There was a problem hiding this comment.
The new UI convenience methods are marked optional (getView?, listViews?, etc.), but the deprecation guidance/docs in this PR instruct consumers to call them directly as the replacement for IUIService. With the current typing, consumers will need optional chaining or casts. Either make these methods required (to truly replace IUIService at the contract level) or update the migration/docs to prefer the always-available get('view', name) / list('view') APIs and treat the convenience methods as optional sugar.
| getView?(name: string): Promise<unknown | undefined>; | |
| /** | |
| * Convenience: list view definitions, optionally filtered by object | |
| */ | |
| listViews?(object?: string): Promise<unknown[]>; | |
| /** | |
| * Convenience: get a dashboard definition by name | |
| * Equivalent to get('dashboard', name) | |
| */ | |
| getDashboard?(name: string): Promise<unknown | undefined>; | |
| /** | |
| * Convenience: list all dashboard definitions | |
| */ | |
| listDashboards?(): Promise<unknown[]>; | |
| getView(name: string): Promise<unknown | undefined>; | |
| /** | |
| * Convenience: list view definitions, optionally filtered by object | |
| */ | |
| listViews(object?: string): Promise<unknown[]>; | |
| /** | |
| * Convenience: get a dashboard definition by name | |
| * Equivalent to get('dashboard', name) | |
| */ | |
| getDashboard(name: string): Promise<unknown | undefined>; | |
| /** | |
| * Convenience: list all dashboard definitions | |
| */ | |
| listDashboards(): Promise<unknown[]>; |
IUIServiceis a redundant subset ofIMetadataService. The metadata service already provides a generic(type, name)interface with three-layer overlay resolution, making a separate UI service unnecessary.IMetadataService enhancements
getEffective()now accepts optional auth context for user-scoped overlay resolution:MetadataManager— symmetric with existinggetObject()/listObjects():getView(name)→ delegates toget('view', name)listViews(object?)→list('view')+ optional object filtergetDashboard(name)/listDashboards()Overlay resolution semantics
userIdcontext: applies user overlay ifownermatches or overlay is unscopedowner(generic user-level customizations)IUIService marked
@deprecated_deprecatedfield, behavior unchangedIUIServicestill exported from contracts index — no breaking changeDocumentation & routing
/api/v1/uiroute annotated as deprecatedAPI_IMPLEMENTATION_PLAN.md:plugin-ui-apisection replaced with metadata API equivalentsROADMAP.md: IUIService marked deprecated in contract matrixllms.txt: updated contract tablemetadata-service.mdx: new "UI Metadata" section with overlay/customization examplesTests
getEffectivewith userId-scoped overlay contextOriginal prompt
重构任务:废弃 IUIService,统一到 IMetadataService
背景
IUIService是IMetadataService的一个冗余子集。IMetadataService已经是泛型的(type, name)接口,view/dashboard 只是type参数的不同值。同时IMetadataService已有完整的三层 Overlay 系统(system → platform → user)和getEffective()方法,完全覆盖了 UI 自定义和权限分层的需求。当前
IUIService的所有方法都可以用IMetadataService一对一替代:getView(name)→metadata.get('view', name)listViews(object?)→metadata.list('view')+ filterregisterView(name, def)→metadata.register('view', name, def)getDashboard(name)→metadata.get('dashboard', name)listDashboards()→metadata.list('dashboard')registerDashboard(name, def)→metadata.register('dashboard', name, def)宪法约束
所有修改必须遵循
@objectstack/spec中定义的协议(Zod schemas、contracts、kernel 类型),不得破坏现有 schema 兼容性。需要修改的文件和具体操作
Phase 1:增强 IMetadataService,补齐 UI 场景缺失的能力
1.1 增强
getEffective签名,支持 auth context文件:
packages/spec/src/contracts/metadata-service.ts在
getEffective方法签名中增加可选的 context 参数:这样调用方可以传入当前用户信息,
getEffective能自动定位正确的 user overlay。1.2 在 IMetadataService 中添加 UI convenience methods
文件:
packages/spec/src/contracts/metadata-service.ts在 IMetadataService 接口末尾添加(与已有的
getObject/listObjects对称):1.3 在 MetadataManager 中实现这些 convenience methods
文件:
packages/metadata/src/metadata-manager.ts添加实现(参考已有的
getObject/listObjects模式):1.4 增强
getEffective实现,支持 userId 级别的 overlay 查找文件:
packages/metadata/src/metadata-manager.ts修改现有
getEffective方法:Phase 2:标记 IUIService 为 deprecated
2.1 标记接口 deprecated
文件:
packages/spec/src/contracts/ui-service.ts在
IUIService接口上方添加@deprecatedJSDoc: