Add metadata extension protocol for plugin extensibility#215
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
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
Adds a standardized “extension” protocol so plugins can attach namespaced custom metadata to core definitions (e.g., Objects/Fields) without modifying base schemas.
Changes:
- Introduces
src/system/extension.zod.ts(extension value/map/definition/registry schemas + helper utilities) and accompanying tests. - Integrates optional
extensionsintoFieldSchemaandObjectSchema, plus tests validating extended metadata. - Adds AI extension reference definitions (field/object) with schemas/tests, and updates generated JSON Schemas + docs + a new example.
Reviewed changes
Copilot reviewed 38 out of 38 changed files in this pull request and generated 11 comments.
Show a summary per file
| File | Description |
|---|---|
| packages/spec/src/system/index.ts | Exposes the new extension protocol via the System barrel export. |
| packages/spec/src/system/extension.zod.ts | Defines extension schemas and helper utilities for reading/writing extensions. |
| packages/spec/src/system/extension.test.ts | Adds unit tests for extension schemas and helpers. |
| packages/spec/src/data/object.zod.ts | Adds extensions support to Object metadata. |
| packages/spec/src/data/object.test.ts | Tests Objects with/without extensions and complex extension values. |
| packages/spec/src/data/field.zod.ts | Adds extensions support to Field metadata. |
| packages/spec/src/data/field.test.ts | Tests Fields with/without extensions and complex extension values. |
| packages/spec/src/ai/object-extensions.zod.ts | Provides AI-oriented Object extension definitions + schema. |
| packages/spec/src/ai/index.ts | Exports AI extension definitions/schemas from the AI barrel. |
| packages/spec/src/ai/field-extensions.zod.ts | Provides AI-oriented Field extension definitions + schema. |
| packages/spec/src/ai/extensions.test.ts | Tests AI extension definitions and end-to-end schema integration. |
| packages/spec/json-schema/ui/FieldWidgetProps.json | Updates generated UI schema to include extensions on fields. |
| packages/spec/json-schema/system/ExtensionsMap.json | Adds generated JSON schema for ExtensionsMap. |
| packages/spec/json-schema/system/ExtensionValue.json | Adds generated JSON schema for ExtensionValue. |
| packages/spec/json-schema/system/ExtensionRegistry.json | Adds generated JSON schema for ExtensionRegistry. |
| packages/spec/json-schema/system/ExtensionDefinition.json | Adds generated JSON schema for ExtensionDefinition. |
| packages/spec/json-schema/kernel/Manifest.json | Updates generated Manifest schema to include extensions in embedded object/field definitions. |
| packages/spec/json-schema/hub/ComposerResponse.json | Updates generated Hub schema to include extensions in embedded object/field definitions. |
| packages/spec/json-schema/data/Object.json | Updates generated Object schema to include extensions. |
| packages/spec/json-schema/data/Field.json | Updates generated Field schema to include extensions. |
| packages/spec/json-schema/ai/AIObjectExtension.json | Adds generated schema for AI Object extension keys. |
| packages/spec/json-schema/ai/AIFieldExtension.json | Adds generated schema for AI Field extension keys. |
| examples/ai-customer-service/index.ts | Demonstrates adding extensions to an object/fields and validating with schemas. |
| examples/ai-customer-service/README.md | Documents the AI customer service example usage. |
| content/docs/references/system/meta.json | Registers the new System Extension reference section in docs navigation. |
| content/docs/references/system/extension/meta.json | Adds docs metadata for the Extension reference section. |
| content/docs/references/system/extension/ExtensionsMap.mdx | Adds reference doc stub for ExtensionsMap. |
| content/docs/references/system/extension/ExtensionValue.mdx | Adds reference doc stub for ExtensionValue. |
| content/docs/references/system/extension/ExtensionRegistry.mdx | Adds reference doc for ExtensionRegistry. |
| content/docs/references/system/extension/ExtensionDefinition.mdx | Adds reference doc for ExtensionDefinition. |
| content/docs/references/data/object/Object.mdx | Updates Object reference to mention the new extensions property. |
| content/docs/references/data/field/Field.mdx | Updates Field reference to mention the new extensions property. |
| content/docs/references/ai/object-extensions/meta.json | Adds docs metadata for AI Object extensions reference section. |
| content/docs/references/ai/object-extensions/AIObjectExtension.mdx | Adds reference doc for AI Object extension keys. |
| content/docs/references/ai/meta.json | Registers AI extension reference pages in AI docs navigation. |
| content/docs/references/ai/field-extensions/meta.json | Adds docs metadata for AI Field extensions reference section. |
| content/docs/references/ai/field-extensions/AIFieldExtension.mdx | Adds reference doc for AI Field extension keys. |
| content/docs/guides/metadata-extensions.mdx | Adds a full guide describing how to use the extension protocol and helpers. |
| has: (extensions: ExtensionsMap | undefined, key: string): boolean => { | ||
| return extensions ? key in extensions : false; | ||
| }, |
There was a problem hiding this comment.
Extension.has uses the in operator, which returns true for inherited keys like toString even when the extension was never set. Use an own-property check instead (and consider using a null-prototype object for extension maps) to avoid false positives.
| export const ExtensionsMapSchema = z.record( | ||
| z.string().describe('Namespaced extension key (e.g., "plugin_id.property_name")'), | ||
| ExtensionValueSchema.describe('Extension value (string, number, boolean, object, or array)') | ||
| ).optional().describe('Custom extension properties from plugins and modules'); |
There was a problem hiding this comment.
ExtensionsMapSchema is defined as .optional(), which makes the standalone ExtensionsMap type include undefined and produces an awkward top-level JSON Schema (e.g. anyOf with not: {}). Consider making ExtensionsMapSchema the non-optional record, and apply .optional() only at usage sites (Field/Object). Also consider validating keys with a namespaced-key regex (matching ExtensionDefinitionSchema) rather than plain z.string() to enforce the protocol.
| /** | ||
| * Default value if not specified | ||
| */ | ||
| default: z.any().optional().describe('Default value for this extension'), | ||
|
|
There was a problem hiding this comment.
default is z.any(), which allows non-JSON values even though extension values are documented as JSON-compatible. Consider validating default with ExtensionValueSchema (and similarly restricting schema to an object shape if it’s meant to hold JSON Schema).
| ```typescript | ||
| import { ExtensionsMap } from '@objectstack/spec'; | ||
|
|
There was a problem hiding this comment.
The examples in this guide import ExtensionsMap (and later FieldSchema, ObjectSchema, Extension, etc.) directly from @objectstack/spec, but the package root currently only exports namespaces (see packages/spec/src/index.ts). Update the snippets to use the documented namespace import (import { System, Data, AI } from '@objectstack/spec') or direct subpath imports like @objectstack/spec/system / @objectstack/spec/data so they match the actual public API.
|
|
||
| ### Key Concepts | ||
|
|
||
| 1. **Namespaced Keys**: All extensions use dot notation: `plugin_id.property_name` |
There was a problem hiding this comment.
The guide states extension keys use plugin_id.property_name, but elsewhere (and in the schemas) the convention is namespace.property with the property segment in camelCase. Align this description with the intended convention (e.g. plugin_id.propertyName) to avoid encouraging snake_case property segments.
| get: <T = any>(extensions: ExtensionsMap | undefined, key: string, defaultValue?: T): T | undefined => { | ||
| if (!extensions) return defaultValue; | ||
| return (extensions[key] as T) ?? defaultValue; | ||
| }, |
There was a problem hiding this comment.
Extension.get reads extensions[key] directly, which can return inherited prototype properties (e.g. toString) instead of the provided default. Guard with an own-property check (e.g. Object.hasOwn / hasOwnProperty.call) before returning the stored value, otherwise fall back to defaultValue.
| key: z.string() | ||
| .regex(/^[a-z_][a-z0-9_]*\.[a-zA-Z][a-zA-Z0-9_]*$/) | ||
| .describe('Fully qualified extension key (e.g., "ai_assistant.vectorIndexed")'), |
There was a problem hiding this comment.
The key regex and nearby docs allow property_name and a property segment starting with uppercase, but the file also states the property should be camelCase. Tighten the regex/examples so the namespace is snake_case and the property segment is lowerCamelCase (and keep docs consistent with that convention).
| appliesTo: z.array(z.enum([ | ||
| 'object', 'field', 'view', 'app', 'dashboard', 'report', 'action', 'workflow' | ||
| ])).describe('Metadata types this extension can be applied to'), |
There was a problem hiding this comment.
appliesTo is required but can currently be an empty array, which makes an ExtensionDefinition effectively meaningless. Consider using a non-empty array constraint (e.g. .nonempty()) so every definition declares at least one applicable target.
| extensions: z.record( | ||
| z.string(), | ||
| ExtensionDefinitionSchema | ||
| ).describe('Registry of available extension definitions'), |
There was a problem hiding this comment.
ExtensionRegistrySchema.extensions allows any string as the record key, so the map key can diverge from ExtensionDefinition.key. Consider constraining the record key with the same key regex and adding a refinement that enforces recordKey === definition.key (and optionally that definition.pluginId matches the namespace prefix).
| * Demonstrates ObjectStack metadata extensions for AI capabilities | ||
| */ | ||
|
|
||
| import { ObjectSchema, Extension } from '@objectstack/spec'; |
There was a problem hiding this comment.
This example imports ObjectSchema and Extension from @objectstack/spec, but the package root entrypoint currently only exports namespaces (e.g. Data, System). Use namespace imports (import { Data, System } from '@objectstack/spec') or subpath imports (@objectstack/spec/data, @objectstack/spec/system) so the example can actually run.
Plugins need a standard way to extend core metadata (Objects, Fields) with custom properties without modifying base schemas.
Changes
Extension Protocol (
src/system/extension.zod.ts)plugin_id.property_name)get,set,has,remove)Schema Integration
extensionsfield toFieldSchemaandObjectSchemaAI Extensions (Reference Implementation)
Usage
Documentation
content/docs/guides/metadata-extensions.mdxexamples/ai-customer-service/(complete AI-powered support system)Original prompt
💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more Copilot coding agent tips in the docs.