feat: Plugin-based metadata auto-loading via createAppPlugin#424
feat: Plugin-based metadata auto-loading via createAppPlugin#424
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
- 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>
There was a problem hiding this comment.
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-nodeto load metadata viaObjectLoaderand register anapp.<id>service. - Extended
MetadataRegistrywithlistEntries()for raw wrapper access needed during manifest assembly. - Added tests for app plugin behavior, updated the
@objectstack/objectqltest mock, and refreshedobjectstack.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. |
| 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`; |
There was a problem hiding this comment.
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.
| 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; |
| import { ObjectLoader } from './loader'; | ||
| import * as path from 'path'; | ||
| import * as fs from 'fs'; | ||
| import * as yaml from 'js-yaml'; |
There was a problem hiding this comment.
js-yaml is imported here but not used anywhere in this module. Remove the unused import to avoid unnecessary dependencies/unused-import lint failures.
| import * as yaml from 'js-yaml'; |
| listEntries(type: string): Record<string, unknown>[] { | ||
| if (!this.items[type]) return []; | ||
| return Object.values(this.items[type]); |
There was a problem hiding this comment.
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.
| 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[]; |
| // Verify non-object metadata was collected | ||
| expect(manifest.permissions).toBeDefined(); | ||
| expect(manifest.validations).toBeDefined(); | ||
| }); |
There was a problem hiding this comment.
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.
Object metadata was loaded via a hand-rolled
loadObjects()usingfs.readdirSync+yaml.loadinobjectstack.config.ts, bypassing the upstreamapp.*service discovery protocol. This breaks plugin hot-swap and multi-app auto-registration.New:
createAppPlugin()factory (@objectql/platform-node)Uses
ObjectLoaderto recursively scan a directory, assembles a manifest (objects, views, permissions, workflows, etc.), and registers it as anapp.<id>service. The upstreamObjectQLPluginauto-discovers allapp.*services duringstart()and callsql.registerApp(manifest).Updated:
objectstack.config.tsloadObjects(),fs/yamlimports, andobjects:fieldcreateAppPlugin()in plugins array — pure plugin architectureSupporting changes
MetadataRegistry.listEntries()— raw entry access (preserves wrapper fieldsid,path,package) needed by manifest assembly@objectstack/objectqlmock — added missingtoTitleCaseexport that caused silent ObjectLoader failures for objects without explicit field labels*.app.yml, nested directories, missing dirs, label overridesWarning
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/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)/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)/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
📍 Connect Copilot coding agent with Jira, Azure Boards or Linear to delegate work to Copilot in one click without leaving your project management tool.