Skip to content

refactor: deprecate IUIService, unify into IMetadataService#666

Merged
hotlong merged 3 commits into
mainfrom
copilot/refactor-iuiservice-to-imetadataservice
Feb 13, 2026
Merged

refactor: deprecate IUIService, unify into IMetadataService#666
hotlong merged 3 commits into
mainfrom
copilot/refactor-iuiservice-to-imetadataservice

Conversation

Copilot AI commented Feb 13, 2026

Copy link
Copy Markdown
Contributor

IUIService is a redundant subset of IMetadataService. 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:
    const view = await metadata.getEffective('view', 'account_list', {
      userId: 'user-123',
      roles: ['admin'],
    });
  • UI convenience methods added to interface and MetadataManager — symmetric with existing getObject()/listObjects():
    • getView(name) → delegates to get('view', name)
    • listViews(object?)list('view') + optional object filter
    • getDashboard(name) / listDashboards()

Overlay resolution semantics

  • With userId context: applies user overlay if owner matches or overlay is unscoped
  • Without context: only applies user overlays that have no owner (generic user-level customizations)

IUIService marked @deprecated

  • Full migration guide in JSDoc, removal planned for v4.0.0
  • DevPlugin stub gains _deprecated field, behavior unchanged
  • IUIService still exported from contracts index — no breaking change

Documentation & routing

  • Dispatcher: /api/v1/ui route annotated as deprecated
  • API_IMPLEMENTATION_PLAN.md: plugin-ui-api section replaced with metadata API equivalents
  • ROADMAP.md: IUIService marked deprecated in contract matrix
  • llms.txt: updated contract table
  • metadata-service.mdx: new "UI Metadata" section with overlay/customization examples

Tests

  • 5 tests for UI convenience methods, 1 test for getEffective with userId-scoped overlay context
  • All 195 test files / 5270 tests pass
Original prompt

重构任务:废弃 IUIService,统一到 IMetadataService

背景

IUIServiceIMetadataService 的一个冗余子集。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') + filter
  • 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)

宪法约束

所有修改必须遵循 @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?(type: string, name: string, context?: {
  userId?: string;
  tenantId?: string;
  roles?: string[];
  permissions?: string[];
}): Promise<unknown | undefined>;

这样调用方可以传入当前用户信息,getEffective 能自动定位正确的 user overlay。

1.2 在 IMetadataService 中添加 UI convenience methods

文件: packages/spec/src/contracts/metadata-service.ts

在 IMetadataService 接口末尾添加(与已有的 getObject / listObjects 对称):

// ==========================================
// Convenience: UI Metadata
// ==========================================

/**
 * Convenience: get a view definition by name
 * Equivalent to get('view', name)
 */
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[]>;

1.3 在 MetadataManager 中实现这些 convenience methods

文件: packages/metadata/src/metadata-manager.ts

添加实现(参考已有的 getObject / listObjects 模式):

async getView(name: string): Promise<unknown | undefined> {
  return this.get('view', name);
}

async listViews(object?: string): Promise<unknown[]> {
  const views = await this.list('view');
  if (object) {
    return views.filter((v: any) => v?.object === object);
  }
  return views;
}

async getDashboard(name: string): Promise<unknown | undefined> {
  return this.get('dashboard', name);
}

async listDashboards(): Promise<unknown[]> {
  return this.list('dashboard');
}

1.4 增强 getEffective 实现,支持 userId 级别的 overlay 查找

文件: packages/metadata/src/metadata-manager.ts

修改现有 getEffective 方法:

async getEffective(type: string, name: string, context?: {
  userId?: string;
  tenantId?: string;
  roles?: string[];
  permissions?: string[];
}): Promise<unknown | undefined> {
  const base = await this.get(type, name);
  if (!base) return undefined;

  let effective = { ...(base as Record<string, unknown>) };

  // Apply platform overlay
  const platformOverlay = await this.getOverlay(type, name, 'platform');
  if (platformOverlay?.active && platformOverlay.patch) {
    effective = { ...effective, ...platformOverlay.patch };
  }

  // Apply user overlay (scoped to specific user if context provided)
  if (context?.userId) {
    // Look up user-specific overlay by owner
    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) {
      // Only apply if owner matches (or no owner restriction)
      if (!userOverlay.owner || userOverlay.owner === context.userId) {
        effective = { ...effective, ...userOverlay.patch };
      }
    }
  } else {
    const userOverlay = await this.getOverlay(type, name, 'user');
    if (userOverlay?.active && userOverlay.patch) {
      effective = { ...effective, ...userOverlay.patch };
    }
  }

  return effective;
}

Phase 2:标记 IUIService 为 deprecated

2.1 标记接口 deprecated

文件: packages/spec/src/contracts/ui-service.ts

IUIService 接口上方添加 @deprecated JSDoc:

/**
 * @deprecated Use IMetadataService instead.
 * - 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)
 *
 * IMetadataService provides additional capabilities:
 * - Three-layer overlay (system  platform  user) via getEffective()
 * - User-scoped customizat...

</details>



<!-- START COPILOT CODING AGENT TIPS -->
---

💬 We'd love your input! Share your thoughts on Copilot coding agent in our [2 minute survey](https://gh.io/copilot-coding-agent-survey).

@vercel

vercel Bot commented Feb 13, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
objectstack-play Ready Ready Preview, Comment Feb 13, 2026 3:45pm
spec Ready Ready Preview, Comment Feb 13, 2026 3:45pm

Request Review

Copilot AI and others added 2 commits February 13, 2026 15:36
- 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>
Copilot AI changed the title [WIP] Refactor IUIService into IMetadataService refactor: deprecate IUIService, unify into IMetadataService Feb 13, 2026
Copilot AI requested a review from hotlong February 13, 2026 15:41
@hotlong
hotlong marked this pull request as ready for review February 13, 2026 15:58
Copilot AI review requested due to automatic review settings February 13, 2026 15:58
@hotlong
hotlong merged commit 106c6d6 into main Feb 13, 2026
7 checks passed

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 IUIService as @deprecated and document migration to IMetadataService.
  • Add UI convenience methods (getView, listViews, getDashboard, listDashboards) and extend getEffective() with optional auth context.
  • Implement + test the new IMetadataService behaviors in MetadataManager, 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.

Comment on lines +18 to +24
* - 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)
*

Copilot AI Feb 13, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
* - 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)
*

Copilot uses AI. Check for mistakes.
Comment on lines +239 to 246
/** 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); },

Copilot AI Feb 13, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Copilot uses AI. Check for mistakes.
Comment on lines +294 to +309
## 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');
```

Copilot AI Feb 13, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Copilot uses AI. Check for mistakes.
Comment on lines +535 to +544
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 };

Copilot AI Feb 13, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Copilot uses AI. Check for mistakes.
Comment on lines +188 to +204
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[]>;

Copilot AI Feb 13, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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[]>;

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants