Skip to content

feat: Plugin-based metadata auto-loading via createAppPlugin#424

Merged
hotlong merged 2 commits intomainfrom
copilot/fix-object-metadata-plugin-loading
Mar 20, 2026
Merged

feat: Plugin-based metadata auto-loading via createAppPlugin#424
hotlong merged 2 commits intomainfrom
copilot/fix-object-metadata-plugin-loading

Conversation

Copy link
Contributor

Copilot AI commented Mar 20, 2026

Object metadata was loaded via a hand-rolled loadObjects() using fs.readdirSync + yaml.load in objectstack.config.ts, bypassing the upstream app.* service discovery protocol. This breaks plugin hot-swap and multi-app auto-registration.

New: createAppPlugin() factory (@objectql/platform-node)

Uses ObjectLoader to recursively scan a directory, assembles a manifest (objects, views, permissions, workflows, etc.), and registers it as an app.<id> service. The upstream ObjectQLPlugin auto-discovers all app.* services during start() and calls ql.registerApp(manifest).

import { createAppPlugin } from '@objectql/platform-node';

export default {
    plugins: [
        createAppPlugin({
            id: 'project-tracker',
            dir: path.join(__dirname, 'examples/showcase/project-tracker/src'),
            label: 'Project Tracker',
        }),
        new ObjectQLPlugin(),  // auto-discovers app.project-tracker
        // ...
    ]
};

Updated: objectstack.config.ts

  • Removed loadObjects(), fs/yaml imports, and objects: field
  • Replaced with createAppPlugin() in plugins array — pure plugin architecture

Supporting changes

  • MetadataRegistry.listEntries() — raw entry access (preserves wrapper fields id, path, package) needed by manifest assembly
  • @objectstack/objectql mock — added missing toTitleCase export that caused silent ObjectLoader failures for objects without explicit field labels
  • 8 new tests — service registration, id inference from *.app.yml, nested directories, missing dirs, label overrides
  • ROADMAP.md updated

Warning

Firewall rules blocked me from connecting to one or more addresses (expand for details)

I tried to connect to the following addresses, but was blocked by firewall rules:

  • fastdl.mongodb.org
    • Triggering command: /home/REDACTED/work/_temp/ghcca-node/node/bin/node node /home/REDACTED/work/objectql/objectql/node_modules/.bin/../vitest/vitest.mjs run packages/drivers/ packages/protocols/ --noprofile bash k/_temp/ghcca-node/node/bin/node over.*service tql/README.md (dns block)
    • Triggering command: /home/REDACTED/work/_temp/ghcca-node/node/bin/node node /home/REDACTED/work/objectql/objectql/node_modules/.bin/../vitest/vitest.mjs run wc -l ct-metadata-plugin-loading r /usr/bin/wc --noprofile k/objectql/objec-c tql/node_modulescore.quotePath=false wc -l (dns block)
    • Triggering command: /home/REDACTED/work/_temp/ghcca-node/node/bin/node node /home/REDACTED/work/objectql/objectql/node_modules/.bin/../vitest/vitest.mjs run cat /pro�� de/node/bin/bash bash k/_temp/ghcca-node/node/bin/git --noprofile bash /usr/bin/cat sort (dns block)

If you need me to access, download, or install something from one of these locations, you can either:

Original prompt

This section details on the original issue you should resolve

<issue_title>对象元数据插件化自动加载不生效,需遵循 AppPlugin + ObjectQLPlugin 模式</issue_title>
<issue_description>## 问题描述
当前开发及配置方式,插件中的对象元数据未能按 objectstack 规范实现自动被 ObjectQL 引擎发现与加载,需要手工写加载代码。该做法与最新 objectstack-ai/spec 参考标准不符,严重影响长期可维护性和未来插件热插拔、自动集成能力。

典型错误做法(当前)

  • 仍存在手工扫描对象元数据文件(如 loadObjects、fs.readdirSync/glob 自行拼接元数据对象)的老旧模式。
  • objectstack.config.ts 通过自定义代码调用 loader,或显式构造 objects/app/modules 配置,而不是通过插件注册。
  • apps/元数据不是通过 app.* service,由 ObjectQLPlugin 自动注册到 SchemaRegistry。

正确做法

  • 每个业务模块/应用应由 AppPlugin 包裹并注册服务(如 app.project-tracker),元数据由 ObjectLoader 自动递归扫描加载。
  • objectstack.config.ts 只需 plugins: [ObjectQLPlugin, DriverPlugin, AppPlugin(app)],无需额外手工组装 objects。
  • ObjectQLPlugin 会自动发现所有 app.* services, 调用 ql.registerApp 自动注册 objects/views等所有元类型。
  • 参照 objectstack-ai/spec 的 dev config 和 app-crm/objectstack.config.ts,实现完全插件化的元数据声明和发现。

参考最佳实践(spec 仓库模式)

  • AppPlugin.init 自动 ctx.registerService(app.${id}, manifest+bundle)
  • ObjectQLPlugin.start 自动注册 app.* 服务下所有 objects/metadata,无需重复写加载/扫描代码
  • 示例代码可参考:spec/examples/app-host/objectstack.config.ts 及 app-crm/objectstack.config.ts

建议修正

  1. 移除所有自定义/硬编码对象元数据加载逻辑(如 loadObjects、fs、require/glob ��关代码)。
  2. 每个业务示例/模块通过 ObjectLoader.load(dir) 得到 objects/apps/actions 等,组装为 app manifest。
  3. 通过 AppPlugin(或等效实现)注册,每个 bundle 仅定义 manifest 和 metadata,不主动注册到 engine。
  4. 主入口只需 plugins: [ObjectQLPlugin, ...exampleAppPlugins, ...drivers],无需 objects 字段。
  5. 补充自动化测试,确保热插拔与多 app/discovery 测试。
  6. 更新 CHANGELOG/ROADMAP,说明插件自动注册架构。

相关资料


如需迁移代码片段与落地指导,可在本 issue 下跟进。</issue_description>

Comments on the Issue (you are @copilot in this section)


📍 Connect Copilot coding agent with Jira, Azure Boards or Linear to delegate work to Copilot in one click without leaving your project management tool.

@vercel
Copy link

vercel bot commented Mar 20, 2026

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

Project Deployment Actions Updated (UTC)
objectql Ready Ready Preview, Comment Mar 20, 2026 9:07am

Request Review

- Add createAppPlugin() factory in @objectql/platform-node that loads
  metadata from a directory using ObjectLoader and registers as app.*
  service for upstream ObjectQLPlugin auto-discovery
- Update objectstack.config.ts to use createAppPlugin() instead of
  manual loadObjects() function with fs.readdirSync
- Remove hardcoded loadObjects() and objects: field from config
- Add MetadataRegistry.listEntries() method for raw entry access
- Add toTitleCase to @objectstack/objectql mock for test compatibility
- Add 8 comprehensive tests for createAppPlugin
- Update ROADMAP.md

Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
import { ObjectLoader } from './loader';
import * as path from 'path';
import * as fs from 'fs';
import * as yaml from 'js-yaml';
Copilot AI changed the title [WIP] Fix auto-loading of object metadata plugins according to standards feat: Plugin-based metadata auto-loading via createAppPlugin Mar 20, 2026
Copilot AI requested a review from hotlong March 20, 2026 09:12
@hotlong hotlong marked this pull request as ready for review March 20, 2026 09:15
Copilot AI review requested due to automatic review settings March 20, 2026 09:15
@hotlong hotlong merged commit 593a37a into main Mar 20, 2026
3 checks passed
@hotlong hotlong deleted the copilot/fix-object-metadata-plugin-loading branch March 20, 2026 09:15
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR migrates metadata loading to the upstream app.* service discovery protocol by introducing a filesystem-backed AppPlugin factory (createAppPlugin) and updating the root configuration to use the pure plugin architecture (removing the hand-rolled loadObjects() path).

Changes:

  • Added createAppPlugin() in @objectql/platform-node to load metadata via ObjectLoader and register an app.<id> service.
  • Extended MetadataRegistry with listEntries() for raw wrapper access needed during manifest assembly.
  • Added tests for app plugin behavior, updated the @objectstack/objectql test mock, and refreshed objectstack.config.ts + ROADMAP.md.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
ROADMAP.md Documents completion of plugin-based metadata auto-loading milestone.
packages/foundation/types/src/registry.ts Adds listEntries() to return raw registry items (no .content unwrapping).
packages/foundation/platform-node/test/app-plugin.test.ts New unit tests for createAppPlugin() behavior and registration.
packages/foundation/platform-node/src/index.ts Re-exports createAppPlugin() from the package entrypoint.
packages/foundation/platform-node/src/app-plugin.ts Implements createAppPlugin() + manifest assembly from MetadataRegistry.
packages/foundation/core/test/mocks/@objectstack/objectql.ts Adds missing toTitleCase export required by ObjectLoader in tests.
objectstack.config.ts Removes manual object loading and wires createAppPlugin() into the plugin chain.

Comment on lines +97 to +101
for (const type of metadataTypes) {
const items = registry.list(type);
if (items.length > 0) {
// Pluralize key for array form: view → views, etc.
const key = type.endsWith('s') ? type : `${type}s`;
Copy link

Copilot AI Mar 20, 2026

Choose a reason for hiding this comment

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

The pluralization logic (const key = type.endsWith('s') ? type : ${type}s``) will turn the metadata type data into `datas`. Since ObjectLoader registers `*.data.yml` under type `data`, this likely produces a manifest key that downstream app registration does not recognize. Use an explicit mapping for manifest keys (e.g., data -> data, permission -> permissions, etc.) instead of generic pluralization.

Suggested change
for (const type of metadataTypes) {
const items = registry.list(type);
if (items.length > 0) {
// Pluralize key for array form: view → views, etc.
const key = type.endsWith('s') ? type : `${type}s`;
// Explicit mapping from registry type → manifest key to avoid incorrect
// generic pluralization (e.g. "data" must stay "data", not "datas").
const manifestKeyByType: Record<string, string> = {
view: 'views',
form: 'forms',
permission: 'permissions',
report: 'reports',
workflow: 'workflows',
validation: 'validations',
data: 'data',
page: 'pages',
menu: 'menus',
};
for (const type of metadataTypes) {
const items = registry.list(type);
if (items.length > 0) {
const key = manifestKeyByType[type] ?? type;

Copilot uses AI. Check for mistakes.
import { ObjectLoader } from './loader';
import * as path from 'path';
import * as fs from 'fs';
import * as yaml from 'js-yaml';
Copy link

Copilot AI Mar 20, 2026

Choose a reason for hiding this comment

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

js-yaml is imported here but not used anywhere in this module. Remove the unused import to avoid unnecessary dependencies/unused-import lint failures.

Suggested change
import * as yaml from 'js-yaml';

Copilot uses AI. Check for mistakes.
Comment on lines +92 to +94
listEntries(type: string): Record<string, unknown>[] {
if (!this.items[type]) return [];
return Object.values(this.items[type]);
Copy link

Copilot AI Mar 20, 2026

Choose a reason for hiding this comment

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

listEntries() currently hard-codes the return type to Record<string, unknown>[], which forces callers to cast and loses type safety. Consider making it generic (e.g., listEntries<T = unknown>(type: string): T[]) to match getEntry() / list() patterns and keep strict typing at call sites.

Suggested change
listEntries(type: string): Record<string, unknown>[] {
if (!this.items[type]) return [];
return Object.values(this.items[type]);
listEntries<T = MetadataItem>(type: string): T[] {
if (!this.items[type]) return [];
return Object.values(this.items[type]) as T[];

Copilot uses AI. Check for mistakes.
Comment on lines +216 to +219
// Verify non-object metadata was collected
expect(manifest.permissions).toBeDefined();
expect(manifest.validations).toBeDefined();
});
Copy link

Copilot AI Mar 20, 2026

Choose a reason for hiding this comment

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

This test verifies permissions and validations are surfaced on the assembled manifest, but it does not cover *.data.yml (which is common in the repo, e.g. project-tracker fixtures). Adding a .data.yml case + assertion for the exact manifest key would prevent regressions like data accidentally being exposed as datas.

Copilot generated this review using guidance from repository custom instructions.
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.

对象元数据插件化自动加载不生效,需遵循 AppPlugin + ObjectQLPlugin 模式

3 participants