From 2dc4d7e64d3c347167771d69b735ebaac3a9b9b4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 10 Feb 2026 08:29:33 +0000 Subject: [PATCH 1/8] Initial plan From 493b8784ced7eff5fc5fb0b90e7c412245fde1ae Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 10 Feb 2026 08:33:58 +0000 Subject: [PATCH 2/8] Implement dynamic service discovery in ObjectQL protocol Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com> --- packages/objectql/src/plugin.ts | 5 +- packages/objectql/src/protocol.ts | 124 ++++++++++++++++++++---------- 2 files changed, 88 insertions(+), 41 deletions(-) diff --git a/packages/objectql/src/plugin.ts b/packages/objectql/src/plugin.ts index f6cd7026..42672861 100644 --- a/packages/objectql/src/plugin.ts +++ b/packages/objectql/src/plugin.ts @@ -61,7 +61,10 @@ export class ObjectQLPlugin implements Plugin { }); // Register Protocol Implementation - const protocolShim = new ObjectStackProtocolImplementation(this.ql); + const protocolShim = new ObjectStackProtocolImplementation( + this.ql, + () => ctx.getServices ? ctx.getServices() : new Map() + ); ctx.registerService('protocol', protocolShim); ctx.logger.info('Protocol service registered'); diff --git a/packages/objectql/src/protocol.ts b/packages/objectql/src/protocol.ts index a9bef36b..05b4a6d6 100644 --- a/packages/objectql/src/protocol.ts +++ b/packages/objectql/src/protocol.ts @@ -29,54 +29,98 @@ function simpleHash(str: string): string { export class ObjectStackProtocolImplementation implements ObjectStackProtocol { private engine: IDataEngine; + private getServicesRegistry?: () => Map; - constructor(engine: IDataEngine) { + constructor(engine: IDataEngine, getServicesRegistry?: () => Map) { this.engine = engine; + this.getServicesRegistry = getServicesRegistry; } async getDiscovery() { + // Get registered services from kernel if available + const registeredServices = this.getServicesRegistry ? this.getServicesRegistry() : new Map(); + + // Build dynamic service info + const services: Record = { + // --- Kernel-provided (objectql is an example kernel implementation) --- + metadata: { enabled: true, status: 'degraded' as const, route: '/api/meta', provider: 'objectql', message: 'In-memory registry only; DB persistence not yet implemented' }, + data: { enabled: true, status: 'available' as const, route: '/api/data', provider: 'objectql' }, + analytics: { enabled: true, status: 'available' as const, route: '/api/analytics', provider: 'objectql' }, + }; + + // Define service configuration for dynamic discovery + const serviceConfig: Record = { + auth: { route: '/api/v1/auth', plugin: 'plugin-auth' }, + automation: { route: '/api/v1/automation', plugin: 'plugin-automation', capability: 'workflow' }, + cache: { route: '/api/v1/cache', plugin: 'plugin-redis' }, + queue: { route: '/api/v1/queue', plugin: 'plugin-bullmq' }, + job: { route: '/api/v1/jobs', plugin: 'job-scheduler' }, + ui: { route: '/api/v1/ui', plugin: 'ui-plugin' }, + workflow: { route: '/api/v1/workflow', plugin: 'plugin-workflow', capability: 'workflow' }, + realtime: { route: '/api/v1/realtime', plugin: 'plugin-realtime', capability: 'websockets' }, + notification: { route: '/api/v1/notifications', plugin: 'plugin-notifications', capability: 'notifications' }, + ai: { route: '/api/v1/ai', plugin: 'plugin-ai', capability: 'ai' }, + i18n: { route: '/api/v1/i18n', plugin: 'plugin-i18n', capability: 'i18n' }, + graphql: { route: '/graphql', plugin: 'plugin-graphql', capability: 'graphql' }, + 'file-storage': { route: '/api/v1/storage', plugin: 'plugin-storage', capability: 'files' }, + search: { route: '/api/v1/search', plugin: 'plugin-search', capability: 'search' }, + }; + + // Check which services are actually registered + for (const [serviceName, config] of Object.entries(serviceConfig)) { + if (registeredServices.has(serviceName)) { + // Service is registered and available + services[serviceName] = { + enabled: true, + status: 'available' as const, + route: config.route, + provider: config.plugin, + }; + } else { + // Service is not registered + services[serviceName] = { + enabled: false, + status: 'unavailable' as const, + message: `Install ${config.plugin} to enable`, + }; + } + } + + // Build capabilities based on available services + const capabilities = { + graphql: registeredServices.has('graphql'), + search: registeredServices.has('search'), + websockets: registeredServices.has('realtime'), + files: registeredServices.has('file-storage'), + analytics: true, // Always available via objectql + ai: registeredServices.has('ai'), + workflow: registeredServices.has('workflow') || registeredServices.has('automation'), + notifications: registeredServices.has('notification'), + i18n: registeredServices.has('i18n'), + }; + + // Build endpoints (only include available services) + const endpoints: Record = { + data: '/api/data', + metadata: '/api/meta', + analytics: '/api/analytics', + }; + + // Add routes for available plugin services + for (const [serviceName, config] of Object.entries(serviceConfig)) { + if (registeredServices.has(serviceName)) { + // Map service name to endpoint key (some services use different names) + const endpointKey = serviceName === 'file-storage' ? 'storage' : serviceName; + endpoints[endpointKey] = config.route; + } + } + return { version: '1.0', apiName: 'ObjectStack API', - capabilities: { - graphql: false, - search: false, - websockets: false, - files: false, - analytics: true, - ai: false, - workflow: false, - notifications: false, - i18n: false, - }, - endpoints: { - data: '/api/data', - metadata: '/api/meta', - analytics: '/api/analytics', - }, - services: { - // --- Kernel-provided (objectql is an example kernel implementation) --- - metadata: { enabled: true, status: 'degraded' as const, route: '/api/meta', provider: 'objectql', message: 'In-memory registry only; DB persistence not yet implemented' }, - data: { enabled: true, status: 'available' as const, route: '/api/data', provider: 'objectql' }, - analytics: { enabled: true, status: 'available' as const, route: '/api/analytics', provider: 'objectql' }, - // --- Plugin-provided (kernel does NOT handle these) --- - auth: { enabled: false, status: 'unavailable' as const, message: 'Install an auth plugin (e.g. plugin-auth) to enable' }, - automation: { enabled: false, status: 'unavailable' as const, message: 'Install an automation plugin (e.g. plugin-automation) to enable' }, - // --- Core infrastructure (plugin-provided) --- - cache: { enabled: false, status: 'unavailable' as const, message: 'Install a cache plugin (e.g. plugin-redis) to enable' }, - queue: { enabled: false, status: 'unavailable' as const, message: 'Install a queue plugin (e.g. plugin-bullmq) to enable' }, - job: { enabled: false, status: 'unavailable' as const, message: 'Install a job scheduler plugin to enable' }, - // --- Optional services (all plugin-provided) --- - ui: { enabled: false, status: 'unavailable' as const, message: 'Install a UI plugin to enable' }, - workflow: { enabled: false, status: 'unavailable' as const, message: 'Install a workflow plugin to enable' }, - realtime: { enabled: false, status: 'unavailable' as const, message: 'Install a realtime plugin to enable' }, - notification: { enabled: false, status: 'unavailable' as const, message: 'Install a notification plugin to enable' }, - ai: { enabled: false, status: 'unavailable' as const, message: 'Install an AI plugin to enable' }, - i18n: { enabled: false, status: 'unavailable' as const, message: 'Install an i18n plugin to enable' }, - graphql: { enabled: false, status: 'unavailable' as const, message: 'Install a GraphQL plugin to enable' }, - 'file-storage': { enabled: false, status: 'unavailable' as const, message: 'Install a file-storage plugin to enable' }, - search: { enabled: false, status: 'unavailable' as const, message: 'Install a search plugin to enable' }, - }, + capabilities, + endpoints, + services, }; } From 015df1e77b004f8b58b2166c4f32a277083db0e1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 10 Feb 2026 08:36:12 +0000 Subject: [PATCH 3/8] Add services field to GetDiscoveryResponseSchema and create tests Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com> --- packages/objectql/DISCOVERY_EXAMPLE.md | 104 +++++++++++++ .../objectql/src/protocol-discovery.test.ts | 137 ++++++++++++++++++ packages/spec/src/api/protocol.zod.ts | 3 +- 3 files changed, 243 insertions(+), 1 deletion(-) create mode 100644 packages/objectql/DISCOVERY_EXAMPLE.md create mode 100644 packages/objectql/src/protocol-discovery.test.ts diff --git a/packages/objectql/DISCOVERY_EXAMPLE.md b/packages/objectql/DISCOVERY_EXAMPLE.md new file mode 100644 index 00000000..7dac2b9c --- /dev/null +++ b/packages/objectql/DISCOVERY_EXAMPLE.md @@ -0,0 +1,104 @@ +# Dynamic Service Discovery - Integration Example + +This example demonstrates how the ObjectStack discovery API dynamically reflects registered plugins. + +## Without Auth Plugin + +When no auth plugin is registered: + +```typescript +import { ObjectKernel } from '@objectstack/core'; +import { ObjectQLPlugin } from '@objectstack/objectql'; + +const kernel = new ObjectKernel(); +await kernel.use(new ObjectQLPlugin()); +await kernel.bootstrap(); + +// Get discovery info +const protocol = kernel.getService('protocol'); +const discovery = await protocol.getDiscovery(); + +console.log(discovery.services.auth); +// Output: +// { +// enabled: false, +// status: 'unavailable', +// message: 'Install plugin-auth to enable' +// } + +console.log(discovery.endpoints.auth); // undefined +console.log(discovery.capabilities.workflow); // false +``` + +## With Auth Plugin + +When auth plugin is registered: + +```typescript +import { ObjectKernel } from '@objectstack/core'; +import { ObjectQLPlugin } from '@objectstack/objectql'; +import { AuthPlugin } from '@objectstack/plugin-auth'; + +const kernel = new ObjectKernel(); +await kernel.use(new ObjectQLPlugin()); +await kernel.use(new AuthPlugin({ /* config */ })); +await kernel.bootstrap(); + +// Get discovery info +const protocol = kernel.getService('protocol'); +const discovery = await protocol.getDiscovery(); + +console.log(discovery.services.auth); +// Output: +// { +// enabled: true, +// status: 'available', +// route: '/api/v1/auth', +// provider: 'plugin-auth' +// } + +console.log(discovery.endpoints.auth); // '/api/v1/auth' +``` + +## Client Usage + +The `@objectstack/client` can use discovery to check service availability: + +```typescript +import { ObjectStackClient } from '@objectstack/client'; + +const client = new ObjectStackClient({ + baseUrl: 'http://localhost:3000' +}); + +// Fetch discovery info +const discovery = await client.getDiscovery(); + +// Check if auth is available before using it +if (discovery.services.auth?.enabled) { + // Auth service is available - safe to use + await client.auth.login({ + type: 'email', + email: 'user@example.com', + password: 'password' + }); +} else { + console.log('Auth not available:', discovery.services.auth?.message); +} +``` + +## Dynamic Features + +The discovery system automatically: + +1. **Detects registered services** - Checks kernel service registry +2. **Updates capabilities** - Sets feature flags based on available services +3. **Builds endpoint map** - Only includes routes for available services +4. **Provides provider info** - Shows which plugin provides each service + +## Benefits + +- **Client Adaptation**: Clients can adapt UI/features based on what's available +- **Feature Detection**: No hardcoded assumptions about available services +- **Plugin Discovery**: New plugins automatically appear in discovery +- **Version Independence**: Client and server can have different plugin sets diff --git a/packages/objectql/src/protocol-discovery.test.ts b/packages/objectql/src/protocol-discovery.test.ts new file mode 100644 index 00000000..dc1f2e15 --- /dev/null +++ b/packages/objectql/src/protocol-discovery.test.ts @@ -0,0 +1,137 @@ +// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. + +import { describe, it, expect, beforeEach } from 'vitest'; +import { ObjectStackProtocolImplementation } from './protocol.js'; +import { ObjectQL } from './engine.js'; + +describe('ObjectStackProtocolImplementation - Dynamic Service Discovery', () => { + let protocol: ObjectStackProtocolImplementation; + let engine: ObjectQL; + + beforeEach(() => { + engine = new ObjectQL(); + }); + + it('should return unavailable auth service when no services registered', async () => { + // Create protocol without service registry + protocol = new ObjectStackProtocolImplementation(engine); + + const discovery = await protocol.getDiscovery(); + + expect(discovery.services.auth).toBeDefined(); + expect(discovery.services.auth.enabled).toBe(false); + expect(discovery.services.auth.status).toBe('unavailable'); + expect(discovery.services.auth.message).toContain('plugin-auth'); + expect(discovery.capabilities.workflow).toBe(false); + }); + + it('should return available auth service when auth is registered', async () => { + // Mock service registry with auth service + const mockServices = new Map(); + mockServices.set('auth', { /* mock auth service */ }); + + protocol = new ObjectStackProtocolImplementation(engine, () => mockServices); + + const discovery = await protocol.getDiscovery(); + + expect(discovery.services.auth).toBeDefined(); + expect(discovery.services.auth.enabled).toBe(true); + expect(discovery.services.auth.status).toBe('available'); + expect(discovery.services.auth.route).toBe('/api/v1/auth'); + expect(discovery.services.auth.provider).toBe('plugin-auth'); + expect(discovery.endpoints.auth).toBe('/api/v1/auth'); + }); + + it('should return available workflow when automation service is registered', async () => { + const mockServices = new Map(); + mockServices.set('automation', { /* mock automation service */ }); + + protocol = new ObjectStackProtocolImplementation(engine, () => mockServices); + + const discovery = await protocol.getDiscovery(); + + expect(discovery.services.automation).toBeDefined(); + expect(discovery.services.automation.enabled).toBe(true); + expect(discovery.services.automation.status).toBe('available'); + expect(discovery.capabilities.workflow).toBe(true); + }); + + it('should return multiple available services when registered', async () => { + const mockServices = new Map(); + mockServices.set('auth', {}); + mockServices.set('realtime', {}); + mockServices.set('ai', {}); + + protocol = new ObjectStackProtocolImplementation(engine, () => mockServices); + + const discovery = await protocol.getDiscovery(); + + // Check auth + expect(discovery.services.auth.enabled).toBe(true); + expect(discovery.services.auth.status).toBe('available'); + + // Check realtime + expect(discovery.services.realtime.enabled).toBe(true); + expect(discovery.services.realtime.status).toBe('available'); + expect(discovery.capabilities.websockets).toBe(true); + + // Check AI + expect(discovery.services.ai.enabled).toBe(true); + expect(discovery.services.ai.status).toBe('available'); + expect(discovery.capabilities.ai).toBe(true); + + // Endpoints should include available services + expect(discovery.endpoints.auth).toBe('/api/v1/auth'); + expect(discovery.endpoints.realtime).toBe('/api/v1/realtime'); + expect(discovery.endpoints.ai).toBe('/api/v1/ai'); + }); + + it('should always show core services as available', async () => { + protocol = new ObjectStackProtocolImplementation(engine); + + const discovery = await protocol.getDiscovery(); + + // Core services should always be available + expect(discovery.services.metadata.enabled).toBe(true); + expect(discovery.services.metadata.status).toBe('degraded'); + expect(discovery.services.data.enabled).toBe(true); + expect(discovery.services.data.status).toBe('available'); + expect(discovery.services.analytics.enabled).toBe(true); + expect(discovery.services.analytics.status).toBe('available'); + + // Core capabilities + expect(discovery.capabilities.analytics).toBe(true); + }); + + it('should map file-storage service to storage endpoint', async () => { + const mockServices = new Map(); + mockServices.set('file-storage', {}); + + protocol = new ObjectStackProtocolImplementation(engine, () => mockServices); + + const discovery = await protocol.getDiscovery(); + + expect(discovery.services['file-storage'].enabled).toBe(true); + expect(discovery.services['file-storage'].status).toBe('available'); + expect(discovery.endpoints.storage).toBe('/api/v1/storage'); + expect(discovery.capabilities.files).toBe(true); + }); + + it('should handle workflow capability from either automation or workflow service', async () => { + // Test with workflow service + const mockServicesWithWorkflow = new Map(); + mockServicesWithWorkflow.set('workflow', {}); + + protocol = new ObjectStackProtocolImplementation(engine, () => mockServicesWithWorkflow); + let discovery = await protocol.getDiscovery(); + expect(discovery.capabilities.workflow).toBe(true); + + // Test with automation service + const mockServicesWithAutomation = new Map(); + mockServicesWithAutomation.set('automation', {}); + + protocol = new ObjectStackProtocolImplementation(engine, () => mockServicesWithAutomation); + discovery = await protocol.getDiscovery(); + expect(discovery.capabilities.workflow).toBe(true); + }); +}); diff --git a/packages/spec/src/api/protocol.zod.ts b/packages/spec/src/api/protocol.zod.ts index b5cb67f9..361cd5ba 100644 --- a/packages/spec/src/api/protocol.zod.ts +++ b/packages/spec/src/api/protocol.zod.ts @@ -2,7 +2,7 @@ import { z } from 'zod'; import { ViewSchema } from '../ui/view.zod'; -import { ApiCapabilitiesSchema, ApiRoutesSchema } from './discovery.zod'; +import { ApiCapabilitiesSchema, ApiRoutesSchema, ServiceInfoSchema } from './discovery.zod'; import { BatchUpdateRequestSchema, BatchUpdateResponseSchema, BatchOptionsSchema } from './batch.zod'; import { MetadataCacheRequestSchema, MetadataCacheResponseSchema } from './http-cache.zod'; import { QuerySchema } from '../data/query.zod'; @@ -95,6 +95,7 @@ export const GetDiscoveryResponseSchema = z.object({ apiName: z.string().describe('API name'), capabilities: ApiCapabilitiesSchema.optional().describe('Supported features/capabilities'), endpoints: ApiRoutesSchema.optional().describe('Available endpoint paths'), + services: z.record(z.string(), ServiceInfoSchema).optional().describe('Per-service availability map'), }); /** From 4c271d0b8d1816fd1fc5164107338dba39ad81ca Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 10 Feb 2026 08:37:31 +0000 Subject: [PATCH 4/8] Add discovery test and documentation to minimal-auth example Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com> --- examples/minimal-auth/README.md | 59 ++++++++++++++++- examples/minimal-auth/src/test-discovery.ts | 73 +++++++++++++++++++++ 2 files changed, 130 insertions(+), 2 deletions(-) create mode 100644 examples/minimal-auth/src/test-discovery.ts diff --git a/examples/minimal-auth/README.md b/examples/minimal-auth/README.md index fa4fb27a..9a752bb3 100644 --- a/examples/minimal-auth/README.md +++ b/examples/minimal-auth/README.md @@ -67,6 +67,14 @@ This will: 4. Get the current session 5. Test password reset flow +### 5. Test Dynamic Discovery (Optional) + +```bash +pnpm tsx src/test-discovery.ts +``` + +This test demonstrates how the auth service automatically appears in the API discovery response when `plugin-auth` is registered. Before the plugin is registered, `discovery.services.auth.status` is "unavailable". After registration, it becomes "available" with the proper route information. + ## Usage ### Using the ObjectStack Client @@ -122,12 +130,59 @@ curl http://localhost:3000/api/v1/auth/get-session \ ``` minimal-auth/ ├── src/ -│ ├── server.ts # Server setup with AuthPlugin -│ └── test-auth.ts # Authentication flow test +│ ├── server.ts # Server setup with AuthPlugin +│ ├── test-auth.ts # Authentication flow test +│ └── test-discovery.ts # Discovery API test (dynamic service detection) ├── package.json └── README.md ``` +## Dynamic Service Discovery + +ObjectStack features a **dynamic service discovery** system that automatically reflects which plugins are registered. This is particularly useful for clients that need to adapt their UI or behavior based on available services. + +**Discovery Response Without Auth Plugin:** +```json +{ + "services": { + "auth": { + "enabled": false, + "status": "unavailable", + "message": "Install plugin-auth to enable" + } + } +} +``` + +**Discovery Response With Auth Plugin:** +```json +{ + "services": { + "auth": { + "enabled": true, + "status": "available", + "route": "/api/v1/auth", + "provider": "plugin-auth" + } + }, + "endpoints": { + "auth": "/api/v1/auth" + } +} +``` + +Clients can use this to check service availability: +```typescript +const discovery = await client.getDiscovery(); +if (discovery.services.auth?.enabled) { + // Auth is available - show login UI + await client.auth.login({ ... }); +} else { + // Auth not available - hide login UI + console.log(discovery.services.auth?.message); +} +``` + ## Advanced Configuration See `src/server.ts` for examples of enabling advanced features: diff --git a/examples/minimal-auth/src/test-discovery.ts b/examples/minimal-auth/src/test-discovery.ts new file mode 100644 index 00000000..21d23e54 --- /dev/null +++ b/examples/minimal-auth/src/test-discovery.ts @@ -0,0 +1,73 @@ +// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. + +/** + * Discovery Test + * + * This test verifies that the discovery endpoint correctly reflects + * the availability of the auth service when plugin-auth is registered. + */ + +import { ObjectKernel } from '@objectstack/core'; +import { ObjectQL } from '@objectstack/objectql'; +import { ObjectQLPlugin } from '@objectstack/objectql'; +import { InMemoryDriver } from '@objectstack/driver-memory'; +import { AuthPlugin } from '@objectstack/plugin-auth'; + +async function testDiscovery() { + console.log('🧪 Testing Discovery with Auth Plugin...\n'); + + // 1. Create ObjectQL instance with in-memory driver + const objectql = new ObjectQL(); + await objectql.registerDriver(new InMemoryDriver()); + + // 2. Create kernel + const kernel = new ObjectKernel(); + + // 3. Register ObjectQL plugin (which provides protocol service) + await kernel.use(new ObjectQLPlugin(objectql)); + + // 4. Get discovery BEFORE auth is registered + console.log('📋 Discovery BEFORE auth plugin:'); + let protocol = kernel.getService('protocol'); + let discovery = await protocol.getDiscovery(); + console.log(' - Auth service enabled:', discovery.services?.auth?.enabled); + console.log(' - Auth service status:', discovery.services?.auth?.status); + console.log(' - Auth endpoint:', discovery.endpoints?.auth || 'undefined'); + console.log(''); + + // 5. Register auth plugin + await kernel.use(new AuthPlugin({ + secret: 'test-secret-min-32-characters-long-for-jwt-signing', + baseUrl: 'http://localhost:3000', + })); + + // 6. Get discovery AFTER auth is registered + console.log('📋 Discovery AFTER auth plugin:'); + protocol = kernel.getService('protocol'); + discovery = await protocol.getDiscovery(); + console.log(' - Auth service enabled:', discovery.services?.auth?.enabled); + console.log(' - Auth service status:', discovery.services?.auth?.status); + console.log(' - Auth service route:', discovery.services?.auth?.route); + console.log(' - Auth service provider:', discovery.services?.auth?.provider); + console.log(' - Auth endpoint:', discovery.endpoints?.auth); + console.log(''); + + // 7. Verify the results + if (discovery.services?.auth?.enabled && discovery.services?.auth?.status === 'available') { + console.log('✅ SUCCESS: Auth service is correctly shown as available!'); + console.log('✅ Discovery endpoint is working correctly!'); + } else { + console.log('❌ FAILED: Auth service should be available but is not!'); + console.log(' Actual status:', discovery.services?.auth); + process.exit(1); + } + + // 8. Clean up + await kernel.shutdown(); + console.log('\n🎉 All tests passed!'); +} + +testDiscovery().catch((error) => { + console.error('❌ Error:', error); + process.exit(1); +}); From 68b01f744f14ae8ca6871f5d1303f52f513c08f1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 10 Feb 2026 08:38:45 +0000 Subject: [PATCH 5/8] Add implementation summary documentation Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com> --- DYNAMIC_DISCOVERY_IMPLEMENTATION.md | 193 ++++++++++++++++++++++++++++ 1 file changed, 193 insertions(+) create mode 100644 DYNAMIC_DISCOVERY_IMPLEMENTATION.md diff --git a/DYNAMIC_DISCOVERY_IMPLEMENTATION.md b/DYNAMIC_DISCOVERY_IMPLEMENTATION.md new file mode 100644 index 00000000..09ed1bd1 --- /dev/null +++ b/DYNAMIC_DISCOVERY_IMPLEMENTATION.md @@ -0,0 +1,193 @@ +# Dynamic Service Discovery Implementation Summary + +## Overview + +This implementation adds **dynamic service discovery** to ObjectStack, allowing the API discovery endpoint to automatically reflect which plugins are registered at runtime. This is a critical feature for building adaptive clients that can adjust their UI and functionality based on available backend services. + +## Problem Solved + +Previously, the discovery endpoint returned hardcoded service statuses: +- Auth service was always marked as "unavailable" even when plugin-auth was registered +- Clients couldn't detect which services were actually available +- The discovery response didn't reflect the actual runtime configuration + +## Solution + +### 1. Dynamic Service Registry Access + +**Location**: `packages/objectql/src/protocol.ts` + +The `ObjectStackProtocolImplementation` now accepts an optional `getServicesRegistry` callback: + +```typescript +constructor(engine: IDataEngine, getServicesRegistry?: () => Map) { + this.engine = engine; + this.getServicesRegistry = getServicesRegistry; +} +``` + +### 2. Runtime Service Detection + +The `getDiscovery()` method now: +1. Queries the kernel's service registry +2. Checks which services are actually registered +3. Returns `enabled: true, status: 'available'` for registered services +4. Returns `enabled: false, status: 'unavailable'` for missing services +5. Dynamically builds the capabilities and endpoints maps + +### 3. Schema Enhancement + +**Location**: `packages/spec/src/api/protocol.zod.ts` + +Added `services` field to `GetDiscoveryResponseSchema`: + +```typescript +export const GetDiscoveryResponseSchema = z.object({ + version: z.string(), + apiName: z.string(), + capabilities: ApiCapabilitiesSchema.optional(), + endpoints: ApiRoutesSchema.optional(), + services: z.record(z.string(), ServiceInfoSchema).optional(), // NEW +}); +``` + +## Usage Example + +### Server Side + +```typescript +import { ObjectKernel } from '@objectstack/core'; +import { ObjectQLPlugin } from '@objectstack/objectql'; +import { AuthPlugin } from '@objectstack/plugin-auth'; + +const kernel = new ObjectKernel(); +await kernel.use(new ObjectQLPlugin()); +await kernel.use(new AuthPlugin({ ... })); +await kernel.bootstrap(); + +// Protocol automatically detects auth service is registered +``` + +### Client Side + +```typescript +import { ObjectStackClient } from '@objectstack/client'; + +const client = new ObjectStackClient({ baseUrl: 'http://localhost:3000' }); +const discovery = await client.getDiscovery(); + +if (discovery.services.auth?.enabled) { + // Show login UI - auth is available + await client.auth.login({ ... }); +} else { + // Hide login UI - auth not installed + console.log(discovery.services.auth?.message); +} +``` + +## Discovery Response Example + +### Without Auth Plugin + +```json +{ + "version": "1.0", + "apiName": "ObjectStack API", + "capabilities": { + "analytics": true, + "workflow": false + }, + "endpoints": { + "data": "/api/data", + "metadata": "/api/meta" + }, + "services": { + "auth": { + "enabled": false, + "status": "unavailable", + "message": "Install plugin-auth to enable" + } + } +} +``` + +### With Auth Plugin + +```json +{ + "version": "1.0", + "apiName": "ObjectStack API", + "capabilities": { + "analytics": true, + "workflow": false + }, + "endpoints": { + "data": "/api/data", + "metadata": "/api/meta", + "auth": "/api/v1/auth" + }, + "services": { + "auth": { + "enabled": true, + "status": "available", + "route": "/api/v1/auth", + "provider": "plugin-auth" + } + } +} +``` + +## Files Changed + +### Core Implementation +1. `packages/objectql/src/protocol.ts` - Dynamic discovery logic +2. `packages/objectql/src/plugin.ts` - Pass service registry to protocol +3. `packages/spec/src/api/protocol.zod.ts` - Add services field to schema + +### Tests +4. `packages/objectql/src/protocol-discovery.test.ts` - Unit tests +5. `examples/minimal-auth/src/test-discovery.ts` - Integration test + +### Documentation +6. `packages/objectql/DISCOVERY_EXAMPLE.md` - Technical documentation +7. `examples/minimal-auth/README.md` - User documentation with examples + +## Benefits + +1. **Client Adaptation**: Clients can detect available features and adapt UI accordingly +2. **Progressive Enhancement**: Enable features when plugins are available, degrade gracefully when not +3. **Type Safety**: Full TypeScript support with Zod schema validation +4. **Zero Configuration**: Works automatically when plugins are registered +5. **Extensible**: New plugins automatically appear in discovery + +## Testing + +Run the tests to verify the implementation: + +```bash +# Unit tests (when vitest is available) +pnpm test + +# Integration test +cd examples/minimal-auth +pnpm tsx src/test-discovery.ts +``` + +## Related Files + +- **Discovery Schema**: `packages/spec/src/api/discovery.zod.ts` +- **Service Info Schema**: Part of discovery.zod.ts +- **Auth Plugin**: `packages/plugins/plugin-auth/src/auth-plugin.ts` +- **Client Implementation**: `packages/client/src/index.ts` + +## Future Enhancements + +- [ ] Add service health checks (beyond just enabled/disabled) +- [ ] Support service versioning in discovery +- [ ] Cache discovery responses for performance +- [ ] Add service dependency graph to discovery +- [ ] Support partial service availability (degraded mode) + +## Conclusion + +This implementation completes the dynamic service discovery feature, enabling ObjectStack to properly report plugin availability at runtime. The auth plugin now correctly appears in the discovery endpoint when registered, and clients can use this information to adapt their behavior accordingly. From 3f83f4af886b2c7898fb39b2a0c8c4a803716f44 Mon Sep 17 00:00:00 2001 From: Jack Zhuang <50353452+hotlong@users.noreply.github.com> Date: Tue, 10 Feb 2026 16:39:22 +0800 Subject: [PATCH 6/8] =?UTF-8?q?=E5=88=A0=E9=99=A4=20DYNAMIC=5FDISCOVERY=5F?= =?UTF-8?q?IMPLEMENTATION.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- DYNAMIC_DISCOVERY_IMPLEMENTATION.md | 193 ---------------------------- 1 file changed, 193 deletions(-) delete mode 100644 DYNAMIC_DISCOVERY_IMPLEMENTATION.md diff --git a/DYNAMIC_DISCOVERY_IMPLEMENTATION.md b/DYNAMIC_DISCOVERY_IMPLEMENTATION.md deleted file mode 100644 index 09ed1bd1..00000000 --- a/DYNAMIC_DISCOVERY_IMPLEMENTATION.md +++ /dev/null @@ -1,193 +0,0 @@ -# Dynamic Service Discovery Implementation Summary - -## Overview - -This implementation adds **dynamic service discovery** to ObjectStack, allowing the API discovery endpoint to automatically reflect which plugins are registered at runtime. This is a critical feature for building adaptive clients that can adjust their UI and functionality based on available backend services. - -## Problem Solved - -Previously, the discovery endpoint returned hardcoded service statuses: -- Auth service was always marked as "unavailable" even when plugin-auth was registered -- Clients couldn't detect which services were actually available -- The discovery response didn't reflect the actual runtime configuration - -## Solution - -### 1. Dynamic Service Registry Access - -**Location**: `packages/objectql/src/protocol.ts` - -The `ObjectStackProtocolImplementation` now accepts an optional `getServicesRegistry` callback: - -```typescript -constructor(engine: IDataEngine, getServicesRegistry?: () => Map) { - this.engine = engine; - this.getServicesRegistry = getServicesRegistry; -} -``` - -### 2. Runtime Service Detection - -The `getDiscovery()` method now: -1. Queries the kernel's service registry -2. Checks which services are actually registered -3. Returns `enabled: true, status: 'available'` for registered services -4. Returns `enabled: false, status: 'unavailable'` for missing services -5. Dynamically builds the capabilities and endpoints maps - -### 3. Schema Enhancement - -**Location**: `packages/spec/src/api/protocol.zod.ts` - -Added `services` field to `GetDiscoveryResponseSchema`: - -```typescript -export const GetDiscoveryResponseSchema = z.object({ - version: z.string(), - apiName: z.string(), - capabilities: ApiCapabilitiesSchema.optional(), - endpoints: ApiRoutesSchema.optional(), - services: z.record(z.string(), ServiceInfoSchema).optional(), // NEW -}); -``` - -## Usage Example - -### Server Side - -```typescript -import { ObjectKernel } from '@objectstack/core'; -import { ObjectQLPlugin } from '@objectstack/objectql'; -import { AuthPlugin } from '@objectstack/plugin-auth'; - -const kernel = new ObjectKernel(); -await kernel.use(new ObjectQLPlugin()); -await kernel.use(new AuthPlugin({ ... })); -await kernel.bootstrap(); - -// Protocol automatically detects auth service is registered -``` - -### Client Side - -```typescript -import { ObjectStackClient } from '@objectstack/client'; - -const client = new ObjectStackClient({ baseUrl: 'http://localhost:3000' }); -const discovery = await client.getDiscovery(); - -if (discovery.services.auth?.enabled) { - // Show login UI - auth is available - await client.auth.login({ ... }); -} else { - // Hide login UI - auth not installed - console.log(discovery.services.auth?.message); -} -``` - -## Discovery Response Example - -### Without Auth Plugin - -```json -{ - "version": "1.0", - "apiName": "ObjectStack API", - "capabilities": { - "analytics": true, - "workflow": false - }, - "endpoints": { - "data": "/api/data", - "metadata": "/api/meta" - }, - "services": { - "auth": { - "enabled": false, - "status": "unavailable", - "message": "Install plugin-auth to enable" - } - } -} -``` - -### With Auth Plugin - -```json -{ - "version": "1.0", - "apiName": "ObjectStack API", - "capabilities": { - "analytics": true, - "workflow": false - }, - "endpoints": { - "data": "/api/data", - "metadata": "/api/meta", - "auth": "/api/v1/auth" - }, - "services": { - "auth": { - "enabled": true, - "status": "available", - "route": "/api/v1/auth", - "provider": "plugin-auth" - } - } -} -``` - -## Files Changed - -### Core Implementation -1. `packages/objectql/src/protocol.ts` - Dynamic discovery logic -2. `packages/objectql/src/plugin.ts` - Pass service registry to protocol -3. `packages/spec/src/api/protocol.zod.ts` - Add services field to schema - -### Tests -4. `packages/objectql/src/protocol-discovery.test.ts` - Unit tests -5. `examples/minimal-auth/src/test-discovery.ts` - Integration test - -### Documentation -6. `packages/objectql/DISCOVERY_EXAMPLE.md` - Technical documentation -7. `examples/minimal-auth/README.md` - User documentation with examples - -## Benefits - -1. **Client Adaptation**: Clients can detect available features and adapt UI accordingly -2. **Progressive Enhancement**: Enable features when plugins are available, degrade gracefully when not -3. **Type Safety**: Full TypeScript support with Zod schema validation -4. **Zero Configuration**: Works automatically when plugins are registered -5. **Extensible**: New plugins automatically appear in discovery - -## Testing - -Run the tests to verify the implementation: - -```bash -# Unit tests (when vitest is available) -pnpm test - -# Integration test -cd examples/minimal-auth -pnpm tsx src/test-discovery.ts -``` - -## Related Files - -- **Discovery Schema**: `packages/spec/src/api/discovery.zod.ts` -- **Service Info Schema**: Part of discovery.zod.ts -- **Auth Plugin**: `packages/plugins/plugin-auth/src/auth-plugin.ts` -- **Client Implementation**: `packages/client/src/index.ts` - -## Future Enhancements - -- [ ] Add service health checks (beyond just enabled/disabled) -- [ ] Support service versioning in discovery -- [ ] Cache discovery responses for performance -- [ ] Add service dependency graph to discovery -- [ ] Support partial service availability (degraded mode) - -## Conclusion - -This implementation completes the dynamic service discovery feature, enabling ObjectStack to properly report plugin availability at runtime. The auth plugin now correctly appears in the discovery endpoint when registered, and clients can use this information to adapt their behavior accordingly. From 4e57c9ded1ba930d60d53189ea75fe97a3fdc543 Mon Sep 17 00:00:00 2001 From: Jack Zhuang <50353452+hotlong@users.noreply.github.com> Date: Tue, 10 Feb 2026 16:39:41 +0800 Subject: [PATCH 7/8] =?UTF-8?q?=E5=88=A0=E9=99=A4=20DISCOVERY=5FEXAMPLE.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/objectql/DISCOVERY_EXAMPLE.md | 104 ------------------------- 1 file changed, 104 deletions(-) delete mode 100644 packages/objectql/DISCOVERY_EXAMPLE.md diff --git a/packages/objectql/DISCOVERY_EXAMPLE.md b/packages/objectql/DISCOVERY_EXAMPLE.md deleted file mode 100644 index 7dac2b9c..00000000 --- a/packages/objectql/DISCOVERY_EXAMPLE.md +++ /dev/null @@ -1,104 +0,0 @@ -# Dynamic Service Discovery - Integration Example - -This example demonstrates how the ObjectStack discovery API dynamically reflects registered plugins. - -## Without Auth Plugin - -When no auth plugin is registered: - -```typescript -import { ObjectKernel } from '@objectstack/core'; -import { ObjectQLPlugin } from '@objectstack/objectql'; - -const kernel = new ObjectKernel(); -await kernel.use(new ObjectQLPlugin()); -await kernel.bootstrap(); - -// Get discovery info -const protocol = kernel.getService('protocol'); -const discovery = await protocol.getDiscovery(); - -console.log(discovery.services.auth); -// Output: -// { -// enabled: false, -// status: 'unavailable', -// message: 'Install plugin-auth to enable' -// } - -console.log(discovery.endpoints.auth); // undefined -console.log(discovery.capabilities.workflow); // false -``` - -## With Auth Plugin - -When auth plugin is registered: - -```typescript -import { ObjectKernel } from '@objectstack/core'; -import { ObjectQLPlugin } from '@objectstack/objectql'; -import { AuthPlugin } from '@objectstack/plugin-auth'; - -const kernel = new ObjectKernel(); -await kernel.use(new ObjectQLPlugin()); -await kernel.use(new AuthPlugin({ /* config */ })); -await kernel.bootstrap(); - -// Get discovery info -const protocol = kernel.getService('protocol'); -const discovery = await protocol.getDiscovery(); - -console.log(discovery.services.auth); -// Output: -// { -// enabled: true, -// status: 'available', -// route: '/api/v1/auth', -// provider: 'plugin-auth' -// } - -console.log(discovery.endpoints.auth); // '/api/v1/auth' -``` - -## Client Usage - -The `@objectstack/client` can use discovery to check service availability: - -```typescript -import { ObjectStackClient } from '@objectstack/client'; - -const client = new ObjectStackClient({ - baseUrl: 'http://localhost:3000' -}); - -// Fetch discovery info -const discovery = await client.getDiscovery(); - -// Check if auth is available before using it -if (discovery.services.auth?.enabled) { - // Auth service is available - safe to use - await client.auth.login({ - type: 'email', - email: 'user@example.com', - password: 'password' - }); -} else { - console.log('Auth not available:', discovery.services.auth?.message); -} -``` - -## Dynamic Features - -The discovery system automatically: - -1. **Detects registered services** - Checks kernel service registry -2. **Updates capabilities** - Sets feature flags based on available services -3. **Builds endpoint map** - Only includes routes for available services -4. **Provides provider info** - Shows which plugin provides each service - -## Benefits - -- **Client Adaptation**: Clients can adapt UI/features based on what's available -- **Feature Detection**: No hardcoded assumptions about available services -- **Plugin Discovery**: New plugins automatically appear in discovery -- **Version Independence**: Client and server can have different plugin sets From 106d98fbce11da689c8381cedb61a14daaee6e2b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 10 Feb 2026 08:40:19 +0000 Subject: [PATCH 8/8] Address code review feedback - improve type safety and performance Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com> --- packages/objectql/src/protocol.ts | 49 ++++++++++++++++--------------- 1 file changed, 26 insertions(+), 23 deletions(-) diff --git a/packages/objectql/src/protocol.ts b/packages/objectql/src/protocol.ts index 05b4a6d6..37bcb83f 100644 --- a/packages/objectql/src/protocol.ts +++ b/packages/objectql/src/protocol.ts @@ -8,7 +8,7 @@ import type { UpdateManyDataRequest, DeleteManyDataRequest } from '@objectstack/spec/api'; -import type { MetadataCacheRequest, MetadataCacheResponse } from '@objectstack/spec/api'; +import type { MetadataCacheRequest, MetadataCacheResponse, ServiceInfo } from '@objectstack/spec/api'; // We import SchemaRegistry directly since this class lives in the same package import { SchemaRegistry } from './registry.js'; @@ -27,6 +27,27 @@ function simpleHash(str: string): string { return Math.abs(hash).toString(16); } +/** + * Service Configuration for Discovery + * Maps service names to their routes and plugin providers + */ +const SERVICE_CONFIG: Record = { + auth: { route: '/api/v1/auth', plugin: 'plugin-auth' }, + automation: { route: '/api/v1/automation', plugin: 'plugin-automation', capability: 'workflow' }, + cache: { route: '/api/v1/cache', plugin: 'plugin-redis' }, + queue: { route: '/api/v1/queue', plugin: 'plugin-bullmq' }, + job: { route: '/api/v1/jobs', plugin: 'job-scheduler' }, + ui: { route: '/api/v1/ui', plugin: 'ui-plugin' }, + workflow: { route: '/api/v1/workflow', plugin: 'plugin-workflow', capability: 'workflow' }, + realtime: { route: '/api/v1/realtime', plugin: 'plugin-realtime', capability: 'websockets' }, + notification: { route: '/api/v1/notifications', plugin: 'plugin-notifications', capability: 'notifications' }, + ai: { route: '/api/v1/ai', plugin: 'plugin-ai', capability: 'ai' }, + i18n: { route: '/api/v1/i18n', plugin: 'plugin-i18n', capability: 'i18n' }, + graphql: { route: '/graphql', plugin: 'plugin-graphql', capability: 'graphql' }, + 'file-storage': { route: '/api/v1/storage', plugin: 'plugin-storage', capability: 'files' }, + search: { route: '/api/v1/search', plugin: 'plugin-search', capability: 'search' }, +}; + export class ObjectStackProtocolImplementation implements ObjectStackProtocol { private engine: IDataEngine; private getServicesRegistry?: () => Map; @@ -40,34 +61,16 @@ export class ObjectStackProtocolImplementation implements ObjectStackProtocol { // Get registered services from kernel if available const registeredServices = this.getServicesRegistry ? this.getServicesRegistry() : new Map(); - // Build dynamic service info - const services: Record = { + // Build dynamic service info with proper typing + const services: Record = { // --- Kernel-provided (objectql is an example kernel implementation) --- metadata: { enabled: true, status: 'degraded' as const, route: '/api/meta', provider: 'objectql', message: 'In-memory registry only; DB persistence not yet implemented' }, data: { enabled: true, status: 'available' as const, route: '/api/data', provider: 'objectql' }, analytics: { enabled: true, status: 'available' as const, route: '/api/analytics', provider: 'objectql' }, }; - // Define service configuration for dynamic discovery - const serviceConfig: Record = { - auth: { route: '/api/v1/auth', plugin: 'plugin-auth' }, - automation: { route: '/api/v1/automation', plugin: 'plugin-automation', capability: 'workflow' }, - cache: { route: '/api/v1/cache', plugin: 'plugin-redis' }, - queue: { route: '/api/v1/queue', plugin: 'plugin-bullmq' }, - job: { route: '/api/v1/jobs', plugin: 'job-scheduler' }, - ui: { route: '/api/v1/ui', plugin: 'ui-plugin' }, - workflow: { route: '/api/v1/workflow', plugin: 'plugin-workflow', capability: 'workflow' }, - realtime: { route: '/api/v1/realtime', plugin: 'plugin-realtime', capability: 'websockets' }, - notification: { route: '/api/v1/notifications', plugin: 'plugin-notifications', capability: 'notifications' }, - ai: { route: '/api/v1/ai', plugin: 'plugin-ai', capability: 'ai' }, - i18n: { route: '/api/v1/i18n', plugin: 'plugin-i18n', capability: 'i18n' }, - graphql: { route: '/graphql', plugin: 'plugin-graphql', capability: 'graphql' }, - 'file-storage': { route: '/api/v1/storage', plugin: 'plugin-storage', capability: 'files' }, - search: { route: '/api/v1/search', plugin: 'plugin-search', capability: 'search' }, - }; - // Check which services are actually registered - for (const [serviceName, config] of Object.entries(serviceConfig)) { + for (const [serviceName, config] of Object.entries(SERVICE_CONFIG)) { if (registeredServices.has(serviceName)) { // Service is registered and available services[serviceName] = { @@ -107,7 +110,7 @@ export class ObjectStackProtocolImplementation implements ObjectStackProtocol { }; // Add routes for available plugin services - for (const [serviceName, config] of Object.entries(serviceConfig)) { + for (const [serviceName, config] of Object.entries(SERVICE_CONFIG)) { if (registeredServices.has(serviceName)) { // Map service name to endpoint key (some services use different names) const endpointKey = serviceName === 'file-storage' ? 'storage' : serviceName;