From 8901d29efe0aeaf602c5c5511db70929d961e666 Mon Sep 17 00:00:00 2001 From: "anthropic-code-agent[bot]" <242468646+Claude@users.noreply.github.com> Date: Mon, 20 Apr 2026 11:48:21 +0000 Subject: [PATCH 1/5] feat(metadata): implement dual-table projection logic for Phase 1 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Create MetadataProjector service for sys_metadata → type-specific table projections - Add projection functions for object, view, agent, tool, flow metadata types - Integrate projection into DatabaseLoader save() and delete() operations - Register system objects from @objectstack/objectos in MetadataPlugin - Add enableProjection config option (default: true) - Projection enables Studio to query metadata via Object Protocol API Phase 1 implementation complete. Next: Studio integration (Phase 2) Agent-Logs-Url: https://github.com/objectstack-ai/framework/sessions/e95ef127-f74b-4ddd-8a6e-fe86e33fe1b0 Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com> --- OBJECTOS_IMPLEMENTATION.md | 15 +- PHASE_1_IMPLEMENTATION.md | 139 +++++++ packages/metadata/package.json | 1 + packages/metadata/src/index.ts | 3 + .../metadata/src/loaders/database-loader.ts | 32 ++ packages/metadata/src/plugin.ts | 22 +- packages/metadata/src/projection/index.ts | 9 + .../src/projection/metadata-projector.ts | 359 ++++++++++++++++++ 8 files changed, 575 insertions(+), 5 deletions(-) create mode 100644 PHASE_1_IMPLEMENTATION.md create mode 100644 packages/metadata/src/projection/index.ts create mode 100644 packages/metadata/src/projection/metadata-projector.ts diff --git a/OBJECTOS_IMPLEMENTATION.md b/OBJECTOS_IMPLEMENTATION.md index b38922525..b9af55495 100644 --- a/OBJECTOS_IMPLEMENTATION.md +++ b/OBJECTOS_IMPLEMENTATION.md @@ -106,10 +106,17 @@ packages/objectos/ ## Next Steps -### Phase 1: Integration (Immediate) -- [ ] Update `packages/metadata` service to support projection -- [ ] Implement dual-table sync logic -- [ ] Register system objects in runtime bootstrap +### Phase 1: Integration (Immediate) ✅ COMPLETED +- [x] Update `packages/metadata` service to support projection +- [x] Implement dual-table sync logic +- [x] Register system objects in runtime bootstrap + +**Implementation Details:** +- Created `MetadataProjector` service in `packages/metadata/src/projection/` +- Integrated projection into `DatabaseLoader.save()` and `DatabaseLoader.delete()` +- Added projection functions for each metadata type: object, view, agent, tool, flow +- Updated `MetadataPlugin` to register all system objects from `@objectstack/objectos` +- Projection is enabled by default, can be disabled via `enableProjection: false` option ### Phase 2: Studio Integration (Next) - [ ] Update Studio to query type-specific tables diff --git a/PHASE_1_IMPLEMENTATION.md b/PHASE_1_IMPLEMENTATION.md new file mode 100644 index 000000000..ce413e8bf --- /dev/null +++ b/PHASE_1_IMPLEMENTATION.md @@ -0,0 +1,139 @@ +# Phase 1 Implementation Summary: Dual-Table Metadata Projection + +## Overview + +Successfully implemented the dual-table projection logic for the metadata service. This establishes the architectural foundation for treating metadata as queryable data following industry best practices from Salesforce, ServiceNow, and Kubernetes. + +## Architecture + +The dual-table pattern maintains two layers: + +1. **sys_metadata** (Source of Truth) + - Package management + - Version control + - Deployment tracking + - History via sys_metadata_history + +2. **Type-Specific Tables** (Queryable Projections) + - sys_object — Object definitions + - sys_view — View configurations + - sys_agent — AI agent metadata + - sys_tool — AI tool registry + - sys_flow — Automation flows + +## Implementation Details + +### 1. MetadataProjector Service + +Created `packages/metadata/src/projection/metadata-projector.ts` with: + +- **Project transformation functions** for each metadata type +- **Denormalization logic** to flatten complex structures for querying +- **Support for both IDataDriver and IDataEngine** (ObjectQL) +- **Automatic CRUD operations** on projection tables + +Key methods: +- `project(type, name, data)` — Create/update projection +- `deleteProjection(type, name)` — Remove projection +- `transformToProjection(type, name, data)` — Type-specific transformation + +### 2. DatabaseLoader Integration + +Updated `packages/metadata/src/loaders/database-loader.ts`: + +- Added `enableProjection` configuration option (default: true) +- Integrated `MetadataProjector` into save() flow +- Added projection cleanup in delete() flow +- Projection occurs AFTER sys_metadata save (async safety) + +### 3. System Object Registration + +Updated `packages/metadata/src/plugin.ts`: + +- Added dependency on `@objectstack/objectos` +- Registered all system objects from SystemObjects registry +- Objects registered via manifest service during plugin init() +- Includes: sys_object, sys_view, sys_agent, sys_tool, sys_flow + +## Projection Mapping + +Each metadata type is projected with denormalized fields for efficient querying: + +### Object Projection (object → sys_object) +- Complex structures (fields, indexes, validations) → JSON columns +- Capabilities → individual boolean columns for filtering +- Denormalized field_count for sorting/filtering + +### View Projection (view → sys_view) +- Columns, filters, sort, config → JSON columns +- Display options → individual columns +- Object reference for joins + +### Agent Projection (agent → sys_agent) +- Model configuration → individual columns +- Tools, skills → JSON columns +- Memory settings → individual columns + +### Tool Projection (tool → sys_tool) +- Parameters schema → JSON column +- Handler code → text column + +### Flow Projection (flow → sys_flow) +- Nodes, edges, variables → JSON columns +- Trigger configuration → individual columns +- Active status for filtering + +## Benefits Achieved + +1. **Unified Query Protocol**: Metadata can be queried using Object Protocol API +2. **Studio Auto-Generation**: UI can use `/api/v1/data/sys_*` endpoints +3. **Efficient Filtering**: Denormalized fields enable fast queries +4. **Preserved History**: sys_metadata maintains full version control +5. **Package Tracking**: All projections include package_id and managed_by + +## Files Changed + +### New Files +- `packages/metadata/src/projection/metadata-projector.ts` — Projection service +- `packages/metadata/src/projection/index.ts` — Module exports + +### Modified Files +- `packages/metadata/src/loaders/database-loader.ts` — Added projection integration +- `packages/metadata/src/plugin.ts` — Registered system objects +- `packages/metadata/src/index.ts` — Exported projection module +- `packages/metadata/package.json` — Added @objectstack/objectos dependency +- `OBJECTOS_IMPLEMENTATION.md` — Updated Phase 1 status + +## Usage Example + +```typescript +// Projection happens automatically when saving metadata +await metadataService.register('object', 'account', { + name: 'account', + label: 'Account', + fields: { /* ... */ }, + // ... object definition +}); + +// Results in TWO database writes: +// 1. sys_metadata: Full envelope with JSON payload +// 2. sys_object: Denormalized projection with queryable columns + +// Studio can now query via Object Protocol +const objects = await client.data.find('sys_object', { + filter: { namespace: 'crm' }, + sort: 'name', +}); +``` + +## Next Steps + +Phase 2 will focus on Studio integration: +- Update Studio to use type-specific table queries +- Auto-generate metadata management UI +- Leverage existing grid/form/kanban components + +Phase 3 will add: +- Comprehensive test coverage +- Documentation updates +- Migration guides for existing deployments diff --git a/packages/metadata/package.json b/packages/metadata/package.json index f96eae46b..053083df1 100644 --- a/packages/metadata/package.json +++ b/packages/metadata/package.json @@ -39,6 +39,7 @@ ], "dependencies": { "@objectstack/core": "workspace:*", + "@objectstack/objectos": "workspace:*", "@objectstack/spec": "workspace:*", "@objectstack/types": "workspace:*", "chokidar": "^5.0.0", diff --git a/packages/metadata/src/index.ts b/packages/metadata/src/index.ts index 071761ecb..1454e95e7 100644 --- a/packages/metadata/src/index.ts +++ b/packages/metadata/src/index.ts @@ -13,6 +13,9 @@ export { MetadataManager, type WatchCallback, type MetadataManagerOptions } from // Plugin export { MetadataPlugin } from './plugin.js'; +// Projection +export { MetadataProjector, type MetadataProjectorOptions } from './projection/index.js'; + // Loaders export { type MetadataLoader } from './loaders/loader-interface.js'; export { MemoryLoader } from './loaders/memory-loader.js'; diff --git a/packages/metadata/src/loaders/database-loader.ts b/packages/metadata/src/loaders/database-loader.ts index 2f147bdb0..73d724b38 100644 --- a/packages/metadata/src/loaders/database-loader.ts +++ b/packages/metadata/src/loaders/database-loader.ts @@ -24,6 +24,7 @@ import { SysMetadataHistoryObject } from '../objects/sys-metadata-history.object import type { IDataDriver, IDataEngine } from '@objectstack/spec/contracts'; import type { MetadataLoader } from './loader-interface.js'; import { calculateChecksum } from '../utils/metadata-history-utils.js'; +import { MetadataProjector } from '../projection/metadata-projector.js'; /** * Configuration for the DatabaseLoader. @@ -54,6 +55,9 @@ export interface DatabaseLoaderOptions { /** Enable history tracking (default: true) */ trackHistory?: boolean; + + /** Enable metadata projection to type-specific tables (default: true) */ + enableProjection?: boolean; } /** @@ -84,6 +88,8 @@ export class DatabaseLoader implements MetadataLoader { private trackHistory: boolean; private schemaReady = false; private historySchemaReady = false; + private enableProjection: boolean; + private projector?: MetadataProjector; constructor(options: DatabaseLoaderOptions) { if (!options.driver && !options.engine) { @@ -96,6 +102,17 @@ export class DatabaseLoader implements MetadataLoader { this.organizationId = options.organizationId; this.environmentId = options.environmentId; this.trackHistory = options.trackHistory !== false; // Default to true + this.enableProjection = options.enableProjection !== false; // Default to true + + // Initialize projector if projection is enabled + if (this.enableProjection) { + this.projector = new MetadataProjector({ + driver: this.driver, + engine: this.engine, + organizationId: this.organizationId, + environmentId: this.environmentId, + }); + } } // ========================================== @@ -709,6 +726,11 @@ export class DatabaseLoader implements MetadataLoader { previousChecksum ); + // Project to type-specific table + if (this.projector) { + await this.projector.project(type, name, data); + } + return { success: true, path: `datasource://${this.tableName}/${type}/${name}`, @@ -746,6 +768,11 @@ export class DatabaseLoader implements MetadataLoader { 'create' ); + // Project to type-specific table + if (this.projector) { + await this.projector.project(type, name, data); + } + return { success: true, path: `datasource://${this.tableName}/${type}/${name}`, @@ -780,6 +807,11 @@ export class DatabaseLoader implements MetadataLoader { // Delete from the main metadata table using the record's ID await this._delete(this.tableName, existing.id as string); + + // Delete projection from type-specific table + if (this.projector) { + await this.projector.deleteProjection(type, name); + } } } diff --git a/packages/metadata/src/plugin.ts b/packages/metadata/src/plugin.ts index 571e95c60..e3ce52d61 100644 --- a/packages/metadata/src/plugin.ts +++ b/packages/metadata/src/plugin.ts @@ -6,6 +6,7 @@ import { DEFAULT_METADATA_TYPE_REGISTRY } from '@objectstack/spec/kernel'; import type { MetadataPluginConfig } from '@objectstack/spec/kernel'; import { SysMetadataObject } from './objects/sys-metadata.object.js'; import { SysMetadataHistoryObject } from './objects/sys-metadata-history.object.js'; +import { SystemObjects } from '@objectstack/objectos'; export interface MetadataPluginOptions { rootDir?: string; @@ -56,7 +57,10 @@ export class MetadataPlugin implements Plugin { // Register metadata system objects via the manifest service (if available). // MetadataPlugin may init before ObjectQLPlugin, so wrap in try/catch. try { - ctx.getService<{ register(m: any): void }>('manifest').register({ + const manifestService = ctx.getService<{ register(m: any): void }>('manifest'); + + // Register the metadata envelope tables + manifestService.register({ id: 'com.objectstack.metadata', name: 'Metadata', version: '1.0.0', @@ -65,6 +69,22 @@ export class MetadataPlugin implements Plugin { namespace: 'sys', objects: [SysMetadataObject, SysMetadataHistoryObject], }); + + // Register the queryable system objects from @objectstack/objectos + manifestService.register({ + id: 'com.objectstack.objectos', + name: 'ObjectOS System Objects', + version: '1.0.0', + type: 'plugin', + scope: 'platform', + namespace: 'sys', + objects: Object.values(SystemObjects), + }); + + ctx.logger.info('Registered system metadata objects', { + metadata: ['sys_metadata', 'sys_metadata_history'], + objectos: Object.keys(SystemObjects), + }); } catch { // ObjectQL not loaded yet — objects will be discovered via legacy fallback } diff --git a/packages/metadata/src/projection/index.ts b/packages/metadata/src/projection/index.ts new file mode 100644 index 000000000..9ebdcc041 --- /dev/null +++ b/packages/metadata/src/projection/index.ts @@ -0,0 +1,9 @@ +// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. + +/** + * Metadata Projection + * + * Dual-table architecture: sys_metadata → type-specific tables + */ + +export { MetadataProjector, type MetadataProjectorOptions } from './metadata-projector.js'; diff --git a/packages/metadata/src/projection/metadata-projector.ts b/packages/metadata/src/projection/metadata-projector.ts new file mode 100644 index 000000000..80159dd1d --- /dev/null +++ b/packages/metadata/src/projection/metadata-projector.ts @@ -0,0 +1,359 @@ +// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. + +/** + * Metadata Projection Service + * + * Implements the dual-table architecture pattern: + * - sys_metadata: Source of truth for package management, versioning + * - Type-specific tables (sys_object, sys_view, etc.): Queryable projections + * + * When metadata is saved to sys_metadata, this service projects it into + * the appropriate type-specific table so Studio can query it via Object Protocol. + */ + +import type { IDataDriver, IDataEngine } from '@objectstack/spec/contracts'; + +/** + * Configuration for the MetadataProjector + */ +export interface MetadataProjectorOptions { + /** The IDataDriver instance to use for database operations */ + driver?: IDataDriver; + + /** The IDataEngine (ObjectQL) instance — preferred over raw driver */ + engine?: IDataEngine; + + /** Organization ID for multi-tenant isolation */ + organizationId?: string; + + /** Environment ID — null = platform-global, set = env-scoped */ + environmentId?: string; +} + +/** + * MetadataProjector + * + * Handles projection from sys_metadata to type-specific tables. + */ +export class MetadataProjector { + private driver?: IDataDriver; + private engine?: IDataEngine; + private organizationId?: string; + private environmentId?: string; + + // Map of metadata types to their target table names + private readonly typeTableMap: Record = { + object: 'sys_object', + view: 'sys_view', + agent: 'sys_agent', + tool: 'sys_tool', + flow: 'sys_flow', + // Add more as needed: dashboard, app, action, workflow, etc. + }; + + constructor(options: MetadataProjectorOptions) { + if (!options.driver && !options.engine) { + throw new Error('MetadataProjector requires either a driver or engine'); + } + this.driver = options.driver; + this.engine = options.engine; + this.organizationId = options.organizationId; + this.environmentId = options.environmentId; + } + + /** + * Project metadata to type-specific table + */ + async project(type: string, name: string, data: any): Promise { + const targetTable = this.typeTableMap[type]; + if (!targetTable) { + // Not all metadata types have projections (e.g., 'field' might not) + return; + } + + const projectedData = this.transformToProjection(type, name, data); + if (!projectedData) { + return; + } + + try { + // Check if projection already exists + const existing = await this._findOne(targetTable, { + where: { name }, + }); + + if (existing) { + // Update existing projection + await this._update(targetTable, existing.id as string, projectedData); + } else { + // Create new projection + const id = this.generateId(); + await this._create(targetTable, { + id, + ...projectedData, + }); + } + } catch (error) { + // Log but don't fail the main save operation + console.error(`Failed to project ${type}/${name} to ${targetTable}:`, error); + } + } + + /** + * Delete projection from type-specific table + */ + async deleteProjection(type: string, name: string): Promise { + const targetTable = this.typeTableMap[type]; + if (!targetTable) { + return; + } + + try { + // Find the projection + const existing = await this._findOne(targetTable, { + where: { name }, + }); + + if (existing) { + await this._delete(targetTable, existing.id as string); + } + } catch (error) { + console.error(`Failed to delete projection ${type}/${name} from ${targetTable}:`, error); + } + } + + /** + * Transform metadata into projection record + */ + private transformToProjection(type: string, name: string, data: any): Record | null { + const now = new Date().toISOString(); + + switch (type) { + case 'object': + return this.projectObject(name, data, now); + case 'view': + return this.projectView(name, data, now); + case 'agent': + return this.projectAgent(name, data, now); + case 'tool': + return this.projectTool(name, data, now); + case 'flow': + return this.projectFlow(name, data, now); + default: + return null; + } + } + + /** + * Project object metadata to sys_object + */ + private projectObject(name: string, data: any, now: string): Record { + return { + name, + label: data.label || name, + plural_label: data.pluralLabel || data.label || name, + description: data.description || '', + icon: data.icon || 'database', + namespace: data.namespace || 'default', + tags: Array.isArray(data.tags) ? data.tags.join(',') : (data.tags || ''), + active: data.active !== false, + is_system: data.isSystem || false, + abstract: data.abstract || false, + datasource: data.datasource || 'default', + table_name: data.tableName || name, + // Serialize complex structures as JSON + fields_json: data.fields ? JSON.stringify(data.fields) : null, + indexes_json: data.indexes ? JSON.stringify(data.indexes) : null, + validations_json: data.validations ? JSON.stringify(data.validations) : null, + state_machines_json: data.stateMachines ? JSON.stringify(data.stateMachines) : null, + capabilities_json: data.enable ? JSON.stringify(data.enable) : null, + // Denormalized fields + field_count: data.fields ? Object.keys(data.fields).length : 0, + display_name_field: data.displayNameField || null, + title_format: data.titleFormat || null, + compact_layout: Array.isArray(data.compactLayout) ? data.compactLayout.join(',') : (data.compactLayout || null), + // Capabilities (denormalized for easier querying) + track_history: data.enable?.trackHistory || false, + searchable: data.enable?.searchable !== false, + api_enabled: data.enable?.apiEnabled !== false, + files: data.enable?.files || false, + feeds: data.enable?.feeds || false, + activities: data.enable?.activities || false, + trash: data.enable?.trash !== false, + mru: data.enable?.mru !== false, + clone: data.enable?.clone !== false, + // Package management + package_id: data.packageId || null, + managed_by: data.managedBy || 'user', + // Audit + created_by: data.createdBy || null, + created_at: data.createdAt || now, + updated_by: data.updatedBy || null, + updated_at: now, + }; + } + + /** + * Project view metadata to sys_view + */ + private projectView(name: string, data: any, now: string): Record { + return { + name, + label: data.label || name, + description: data.description || '', + object_name: data.object || '', + view_type: data.type || 'grid', + // Serialize configurations as JSON + columns_json: data.columns ? JSON.stringify(data.columns) : null, + filters_json: data.filters ? JSON.stringify(data.filters) : null, + sort_json: data.sort ? JSON.stringify(data.sort) : null, + config_json: data.config ? JSON.stringify(data.config) : null, + // Display options + page_size: data.pageSize || 25, + show_search: data.showSearch !== false, + show_filters: data.showFilters !== false, + // Classification + namespace: data.namespace || 'default', + // Package management + package_id: data.packageId || null, + managed_by: data.managedBy || 'user', + // Audit + created_by: data.createdBy || null, + created_at: data.createdAt || now, + updated_by: data.updatedBy || null, + updated_at: now, + }; + } + + /** + * Project agent metadata to sys_agent + */ + private projectAgent(name: string, data: any, now: string): Record { + return { + name, + label: data.label || name, + description: data.description || '', + agent_type: data.type || 'conversational', + // Model configuration + model: data.model || null, + temperature: data.temperature ?? 0.7, + max_tokens: data.maxTokens || null, + top_p: data.topP || null, + // System prompt + system_prompt: data.systemPrompt || null, + // Tools and skills as JSON + tools_json: data.tools ? JSON.stringify(data.tools) : null, + skills_json: data.skills ? JSON.stringify(data.skills) : null, + // Memory + memory_enabled: data.memoryEnabled || false, + memory_window: data.memoryWindow || 10, + // Classification + namespace: data.namespace || 'default', + // Package management + package_id: data.packageId || null, + managed_by: data.managedBy || 'user', + // Audit + created_by: data.createdBy || null, + created_at: data.createdAt || now, + updated_by: data.updatedBy || null, + updated_at: now, + }; + } + + /** + * Project tool metadata to sys_tool + */ + private projectTool(name: string, data: any, now: string): Record { + return { + name, + label: data.label || name, + description: data.description || '', + // Parameters and implementation + parameters_json: data.parameters ? JSON.stringify(data.parameters) : null, + handler_code: data.handler || null, + // Classification + namespace: data.namespace || 'default', + // Package management + package_id: data.packageId || null, + managed_by: data.managedBy || 'user', + // Audit + created_by: data.createdBy || null, + created_at: data.createdAt || now, + updated_by: data.updatedBy || null, + updated_at: now, + }; + } + + /** + * Project flow metadata to sys_flow + */ + private projectFlow(name: string, data: any, now: string): Record { + return { + name, + label: data.label || name, + description: data.description || '', + flow_type: data.type || 'autolaunched', + // Flow definition + nodes_json: data.nodes ? JSON.stringify(data.nodes) : null, + edges_json: data.edges ? JSON.stringify(data.edges) : null, + variables_json: data.variables ? JSON.stringify(data.variables) : null, + // Trigger configuration + trigger_type: data.triggerType || null, + trigger_object: data.triggerObject || null, + // Status + active: data.active || false, + // Classification + namespace: data.namespace || 'default', + // Package management + package_id: data.packageId || null, + managed_by: data.managedBy || 'user', + // Audit + created_by: data.createdBy || null, + created_at: data.createdAt || now, + updated_by: data.updatedBy || null, + updated_at: now, + }; + } + + // ========================================== + // Internal CRUD helpers (driver vs engine) + // ========================================== + + private async _findOne(table: string, query: Record): Promise | null> { + if (this.engine) { + return this.engine.findOne(table, query as any); + } + return this.driver!.findOne(table, { object: table, ...query } as any); + } + + private async _create(table: string, data: Record): Promise> { + if (this.engine) { + return this.engine.insert(table, data); + } + return this.driver!.create(table, data); + } + + private async _update(table: string, id: string, data: Record): Promise> { + if (this.engine) { + return this.engine.update(table, { id, ...data }); + } + return this.driver!.update(table, id, data); + } + + private async _delete(table: string, id: string): Promise { + if (this.engine) { + return this.engine.delete(table, { where: { id } } as any); + } + return this.driver!.delete(table, id); + } + + /** + * Generate a simple unique ID + */ + private generateId(): string { + if (typeof globalThis.crypto !== 'undefined' && typeof globalThis.crypto.randomUUID === 'function') { + return globalThis.crypto.randomUUID(); + } + return `proj_${Date.now()}_${Math.random().toString(36).substring(2, 10)}`; + } +} From 46752718258fdb45f50b7605342d7182ef5bf589 Mon Sep 17 00:00:00 2001 From: Jack Zhuang <50353452+hotlong@users.noreply.github.com> Date: Mon, 20 Apr 2026 20:13:51 +0800 Subject: [PATCH 2/5] feat(metadata): add objectos workspace dependency to pnpm-lock.yaml --- pnpm-lock.yaml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1fb60438a..add01d28b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -761,6 +761,9 @@ importers: '@objectstack/core': specifier: workspace:* version: link:../core + '@objectstack/objectos': + specifier: workspace:* + version: link:../objectos '@objectstack/spec': specifier: workspace:* version: link:../spec From fe3230dd4ea6783bc6ed7e0e6aaf064831fbee8e Mon Sep 17 00:00:00 2001 From: "anthropic-code-agent[bot]" <242468646+Claude@users.noreply.github.com> Date: Mon, 20 Apr 2026 12:17:37 +0000 Subject: [PATCH 3/5] fix: upgrade minimatch to 10.2.3 and fastify to 5.8.5 to address security vulnerabilities Agent-Logs-Url: https://github.com/objectstack-ai/framework/sessions/f41c7558-a7d5-4ee9-a23f-ca4216fb23e1 Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com> --- package.json | 2 +- packages/adapters/fastify/package.json | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 5dcf9d890..057934754 100644 --- a/package.json +++ b/package.json @@ -43,7 +43,7 @@ "packageManager": "pnpm@10.31.0+sha512.e3927388bfaa8078ceb79b748ffc1e8274e84d75163e67bc22e06c0d3aed43dd153151cbf11d7f8301ff4acb98c68bdc5cadf6989532801ffafe3b3e4a63c268", "pnpm": { "overrides": { - "minimatch@<10.2.1": "10.2.1" + "minimatch@<10.2.3": "10.2.3" }, "ignoredBuiltDependencies": [ "@nestjs/core", diff --git a/packages/adapters/fastify/package.json b/packages/adapters/fastify/package.json index f5032a264..a190c1b83 100644 --- a/packages/adapters/fastify/package.json +++ b/packages/adapters/fastify/package.json @@ -18,11 +18,11 @@ }, "peerDependencies": { "@objectstack/runtime": "workspace:^", - "fastify": "^5.8.2" + "fastify": "^5.8.5" }, "devDependencies": { "@objectstack/runtime": "workspace:*", - "fastify": "^5.8.4", + "fastify": "^5.8.5", "typescript": "^6.0.2", "vitest": "^4.1.4" }, From 93ad23c86e55c7ac78c808590d505da03015bf07 Mon Sep 17 00:00:00 2001 From: Jack Zhuang <50353452+hotlong@users.noreply.github.com> Date: Mon, 20 Apr 2026 20:29:19 +0800 Subject: [PATCH 4/5] fix: update minimatch to 10.2.3 and fastify to 5.8.5 for improved compatibility --- pnpm-lock.yaml | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index add01d28b..1f142ee00 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -5,7 +5,7 @@ settings: excludeLinksFromLockfile: false overrides: - minimatch@<10.2.1: 10.2.1 + minimatch@<10.2.3: 10.2.3 importers: @@ -477,8 +477,8 @@ importers: specifier: workspace:* version: link:../../runtime fastify: - specifier: ^5.8.4 - version: 5.8.4 + specifier: ^5.8.5 + version: 5.8.5 typescript: specifier: ^6.0.2 version: 6.0.2 @@ -5302,8 +5302,8 @@ packages: fast-uri@3.1.0: resolution: {integrity: sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==} - fastify@5.8.4: - resolution: {integrity: sha512-sa42J1xylbBAYUWALSBoyXKPDUvM3OoNOibIefA+Oha57FryXKKCZarA1iDntOCWp3O35voZLuDg2mdODXtPzQ==} + fastify@5.8.5: + resolution: {integrity: sha512-Yqptv59pQzPgQUSIm87hMqHJmdkb1+GPxdE6vW6FRyVE9G86mt7rOghitiU4JHRaTyDUk9pfeKmDeu70lAwM4Q==} fastq@1.20.1: resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==} @@ -6389,9 +6389,9 @@ packages: mingo@7.2.1: resolution: {integrity: sha512-MEIQPOSJS2sVCueyQeE2rzgEeW3HpIIhizPbeuwD4v7+miVj7NI3ZVPqqw8t3YPIWCivpIaXA4KsoRI7koyNOA==} - minimatch@10.2.1: - resolution: {integrity: sha512-MClCe8IL5nRRmawL6ib/eT4oLyeKMGCghibcDWK+J0hh0Q8kqSdia6BvbRMVk6mPa6WqUa5uR2oxt6C5jd533A==} - engines: {node: 20 || >=22} + minimatch@10.2.3: + resolution: {integrity: sha512-Rwi3pnapEqirPSbWbrZaa6N3nmqq4Xer/2XooiOKyV3q12ML06f7MOuc5DVH8ONZIFhwIYQ3yzPH4nt7iWHaTg==} + engines: {node: 18 || 20 || >=22} minimatch@10.2.5: resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==} @@ -10846,7 +10846,7 @@ snapshots: leven: 3.1.0 markdown-it: 14.1.1 mime: 1.6.0 - minimatch: 10.2.1 + minimatch: 10.2.3 parse-semver: 1.1.1 read: 1.0.7 secretlint: 10.2.2 @@ -11767,7 +11767,7 @@ snapshots: fast-uri@3.1.0: {} - fastify@5.8.4: + fastify@5.8.5: dependencies: '@fastify/ajv-compiler': 4.0.5 '@fastify/error': 4.2.0 @@ -11815,7 +11815,7 @@ snapshots: filelist@1.0.6: dependencies: - minimatch: 10.2.1 + minimatch: 10.2.3 fill-range@7.1.1: dependencies: @@ -12051,14 +12051,14 @@ snapshots: dependencies: foreground-child: 3.3.1 jackspeak: 4.2.3 - minimatch: 10.2.1 + minimatch: 10.2.3 minipass: 7.1.3 package-json-from-dist: 1.0.1 path-scurry: 2.0.2 glob@13.0.6: dependencies: - minimatch: 10.2.5 + minimatch: 10.2.3 minipass: 7.1.3 path-scurry: 2.0.2 @@ -13157,7 +13157,7 @@ snapshots: mingo@7.2.1: {} - minimatch@10.2.1: + minimatch@10.2.3: dependencies: brace-expansion: 5.0.5 From 40332f5767d2fa3b21cf2c130a2351d8ebfc62ff Mon Sep 17 00:00:00 2001 From: Jack Zhuang <50353452+hotlong@users.noreply.github.com> Date: Mon, 20 Apr 2026 21:32:06 +0800 Subject: [PATCH 5/5] refactor(metadata): update MetadataProjector to use a scope object for organization and environment IDs test: add localStorage shim for consistent testing behavior across environments --- apps/studio/test/ai-chat-panel.test.tsx | 12 ++--- apps/studio/test/setup.ts | 50 ++++++++++++++++++- .../src/projection/metadata-projector.ts | 10 ++-- 3 files changed, 61 insertions(+), 11 deletions(-) diff --git a/apps/studio/test/ai-chat-panel.test.tsx b/apps/studio/test/ai-chat-panel.test.tsx index eec06c90b..7302bdef8 100644 --- a/apps/studio/test/ai-chat-panel.test.tsx +++ b/apps/studio/test/ai-chat-panel.test.tsx @@ -97,10 +97,10 @@ describe('use-ai-chat-panel', () => { }); it('does not throw when localStorage is unavailable', () => { - const originalSetItem = Storage.prototype.setItem; - Storage.prototype.setItem = () => { throw new Error('QuotaExceeded'); }; + const originalSetItem = localStorage.setItem.bind(localStorage); + localStorage.setItem = () => { throw new Error('QuotaExceeded'); }; expect(() => saveMessages([makeMsg({ id: '1', role: 'user', content: 'A' })])).not.toThrow(); - Storage.prototype.setItem = originalSetItem; + localStorage.setItem = originalSetItem; }); }); }); @@ -260,10 +260,10 @@ describe('Agent Selector', () => { }); it('should not throw when localStorage is unavailable', () => { - const originalSetItem = Storage.prototype.setItem; - Storage.prototype.setItem = () => { throw new Error('QuotaExceeded'); }; + const originalSetItem = localStorage.setItem.bind(localStorage); + localStorage.setItem = () => { throw new Error('QuotaExceeded'); }; expect(() => saveSelectedAgent('metadata_assistant')).not.toThrow(); - Storage.prototype.setItem = originalSetItem; + localStorage.setItem = originalSetItem; }); }); diff --git a/apps/studio/test/setup.ts b/apps/studio/test/setup.ts index 09a3c485d..149b9db6d 100644 --- a/apps/studio/test/setup.ts +++ b/apps/studio/test/setup.ts @@ -7,7 +7,55 @@ import '@testing-library/jest-dom/vitest'; import { cleanup } from '@testing-library/react'; -import { afterEach } from 'vitest'; +import { afterEach, beforeEach } from 'vitest'; + +// Node 22+ ships an experimental built-in `localStorage` that is a plain +// empty object without Storage API methods. When `@vitest-environment +// happy-dom` runs, that global can shadow happy-dom's Storage implementation, +// leaving tests with `localStorage.getItem is not a function`. Install a +// minimal in-memory Storage-compatible shim to guarantee consistent behavior +// across Node versions and environments. +function installStorageShim(): void { + const store = new Map(); + const shim = { + get length() { + return store.size; + }, + clear(): void { + store.clear(); + }, + getItem(key: string): string | null { + return store.has(key) ? (store.get(key) as string) : null; + }, + setItem(key: string, value: string): void { + store.set(String(key), String(value)); + }, + removeItem(key: string): void { + store.delete(key); + }, + key(index: number): string | null { + return Array.from(store.keys())[index] ?? null; + }, + }; + Object.defineProperty(globalThis, 'localStorage', { + configurable: true, + writable: true, + value: shim, + }); + Object.defineProperty(globalThis, 'sessionStorage', { + configurable: true, + writable: true, + value: { ...shim }, + }); +} + +installStorageShim(); + +// Re-install before each test so tests that mutate localStorage.setItem +// (to simulate quota errors) cannot leak across other tests. +beforeEach(() => { + installStorageShim(); +}); // Cleanup after each test afterEach(() => { diff --git a/packages/metadata/src/projection/metadata-projector.ts b/packages/metadata/src/projection/metadata-projector.ts index 80159dd1d..e510e6161 100644 --- a/packages/metadata/src/projection/metadata-projector.ts +++ b/packages/metadata/src/projection/metadata-projector.ts @@ -38,8 +38,8 @@ export interface MetadataProjectorOptions { export class MetadataProjector { private driver?: IDataDriver; private engine?: IDataEngine; - private organizationId?: string; - private environmentId?: string; + /** Reserved for future multi-tenant projection scoping */ + readonly scope: { organizationId?: string; environmentId?: string }; // Map of metadata types to their target table names private readonly typeTableMap: Record = { @@ -57,8 +57,10 @@ export class MetadataProjector { } this.driver = options.driver; this.engine = options.engine; - this.organizationId = options.organizationId; - this.environmentId = options.environmentId; + this.scope = { + organizationId: options.organizationId, + environmentId: options.environmentId, + }; } /**