diff --git a/ROADMAP.md b/ROADMAP.md index 54b06efd7b..cc928838f0 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -166,7 +166,7 @@ ObjectStack v2.0.7 has achieved strong protocol-level maturity (A- grade). The s ### 7.1 `@objectstack/hono` Adapter ✅ -Fully implemented with `createHonoApp()` and `objectStackMiddleware()` — handles discovery, auth, graphql, metadata, data, analytics, automation, storage, packages endpoints. 24 tests. +Fully implemented with `objectStackMiddleware()` — provides kernel injection for Hono apps. Legacy `createHonoApp()` removed in v3.0. 3 tests. ### 7.2 `@objectstack/nextjs` Adapter ✅ @@ -269,14 +269,14 @@ Several deprecated items say "Will be removed in v2.0.0" but current version is - [x] Update CHANGELOG.md with breaking changes - [x] Audit and document all 23 remaining @deprecated items (14 spec + 9 runtime) - [x] Identify stale deprecation notices targeting v2.0.0 -- [ ] Update stale deprecation notices to target v3.0.0 -- [ ] Extract runtime logic from spec → core (3 functions + 3 event helpers) -- [ ] Remove hub/ re-export barrel + `Hub.*` namespace -- [ ] Remove deprecated schema aliases (RateLimitSchema, RealtimePresenceStatus, RealtimeAction) -- [ ] Remove deprecated `location` field from ActionSchema -- [ ] Remove deprecated `capabilities` from DiscoverySchema -- [ ] Remove deprecated compat aliases in runtime packages -- [ ] Tighten `z.any()` in `ui/i18n.zod.ts` to typed union +- [x] Update stale deprecation notices to target v3.0.0 (N/A — items removed in v3.0) +- [x] Extract runtime logic from spec → core (6 functions removed from spec) +- [x] Remove hub/ re-export barrel + `Hub.*` namespace +- [x] Remove deprecated schema aliases (RateLimitSchema, RealtimePresenceStatus, RealtimeAction) +- [x] Remove deprecated `location` field from ActionSchema +- [x] Remove deprecated `capabilities` from DiscoverySchema +- [x] Remove deprecated compat aliases in runtime packages +- [x] Tighten `z.any()` in `ui/i18n.zod.ts` to typed union --- diff --git a/packages/adapters/hono/src/hono.test.ts b/packages/adapters/hono/src/hono.test.ts index 1a0f52b4d4..172929ca7e 100644 --- a/packages/adapters/hono/src/hono.test.ts +++ b/packages/adapters/hono/src/hono.test.ts @@ -3,400 +3,15 @@ import { describe, it, expect, vi, beforeEach } from 'vitest'; import { Hono } from 'hono'; -// Mock dispatcher instance -const mockDispatcher = { - getDiscoveryInfo: vi.fn().mockReturnValue({ version: '1.0', endpoints: [] }), - handleAuth: vi.fn().mockResolvedValue({ handled: true, response: { body: { ok: true }, status: 200 } }), - handleGraphQL: vi.fn().mockResolvedValue({ data: {} }), - handleMetadata: vi.fn().mockResolvedValue({ handled: true, response: { body: { objects: [] }, status: 200 } }), - handleData: vi.fn().mockResolvedValue({ handled: true, response: { body: { records: [] }, status: 200 } }), - handleAnalytics: vi.fn().mockResolvedValue({ handled: true, response: { body: {}, status: 200 } }), - handleAutomation: vi.fn().mockResolvedValue({ handled: true, response: { body: {}, status: 200 } }), - handleStorage: vi.fn().mockResolvedValue({ handled: true, response: { body: {}, status: 200 } }), - handlePackages: vi.fn().mockResolvedValue({ handled: true, response: { body: {}, status: 200 } }), -}; - -vi.mock('@objectstack/runtime', () => { - return { - HttpDispatcher: function HttpDispatcher() { - return mockDispatcher; - }, - }; -}); - -import { createHonoApp, objectStackMiddleware } from './index'; +import { objectStackMiddleware } from './index'; const mockKernel = { name: 'test-kernel' } as any; -describe('createHonoApp', () => { +describe('objectStackMiddleware', () => { beforeEach(() => { vi.clearAllMocks(); }); - it('should return a Hono app instance', () => { - const app = createHonoApp({ kernel: mockKernel }); - expect(app).toBeInstanceOf(Hono); - }); - - describe('Discovery Endpoint', () => { - it('GET /api returns discovery info', async () => { - const app = createHonoApp({ kernel: mockKernel }); - const res = await app.request('/api'); - expect(res.status).toBe(200); - const json = await res.json(); - expect(json.data).toEqual({ version: '1.0', endpoints: [] }); - expect(mockDispatcher.getDiscoveryInfo).toHaveBeenCalledWith('/api'); - }); - - it('uses custom prefix for discovery', async () => { - const app = createHonoApp({ kernel: mockKernel, prefix: '/v2' }); - const res = await app.request('/v2'); - expect(res.status).toBe(200); - expect(mockDispatcher.getDiscoveryInfo).toHaveBeenCalledWith('/v2'); - }); - }); - - describe('Auth Endpoint', () => { - it('POST /api/auth/login calls handleAuth', async () => { - const app = createHonoApp({ kernel: mockKernel }); - const res = await app.request('/api/auth/login', { method: 'POST' }); - expect(res.status).toBe(200); - expect(mockDispatcher.handleAuth).toHaveBeenCalledWith( - 'login', - 'POST', - expect.anything(), - expect.objectContaining({ request: expect.any(Request) }), - ); - }); - - it('GET /api/auth/callback calls handleAuth', async () => { - const app = createHonoApp({ kernel: mockKernel }); - const res = await app.request('/api/auth/callback', { method: 'GET' }); - expect(res.status).toBe(200); - expect(mockDispatcher.handleAuth).toHaveBeenCalledWith( - 'callback', - 'GET', - expect.anything(), - expect.objectContaining({ request: expect.any(Request) }), - ); - }); - - it('returns error on handleAuth exception', async () => { - mockDispatcher.handleAuth.mockRejectedValueOnce( - Object.assign(new Error('Unauthorized'), { statusCode: 401 }), - ); - const app = createHonoApp({ kernel: mockKernel }); - const res = await app.request('/api/auth/login', { method: 'POST' }); - expect(res.status).toBe(401); - const json = await res.json(); - expect(json.success).toBe(false); - expect(json.error.message).toBe('Unauthorized'); - }); - }); - - describe('Auth via AuthPlugin service', () => { - it('uses kernel.getService("auth") when available', async () => { - const mockHandleRequest = vi.fn().mockResolvedValue( - new Response(JSON.stringify({ user: { id: '1' } }), { - status: 200, - headers: { 'Content-Type': 'application/json' }, - }), - ); - const kernelWithAuth = { - ...mockKernel, - getService: vi.fn().mockReturnValue({ handleRequest: mockHandleRequest }), - }; - const app = createHonoApp({ kernel: kernelWithAuth }); - const res = await app.request('/api/auth/sign-in/email', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ email: 'a@b.com', password: 'pass' }), - }); - expect(res.status).toBe(200); - const json = await res.json(); - expect(json.user.id).toBe('1'); - expect(kernelWithAuth.getService).toHaveBeenCalledWith('auth'); - expect(mockHandleRequest).toHaveBeenCalledWith(expect.any(Request)); - expect(mockDispatcher.handleAuth).not.toHaveBeenCalled(); - }); - - it('falls back to dispatcher.handleAuth when auth service is not available', async () => { - const kernelWithoutAuth = { - ...mockKernel, - getService: vi.fn().mockReturnValue(null), - }; - const app = createHonoApp({ kernel: kernelWithoutAuth }); - const res = await app.request('/api/auth/login', { method: 'POST' }); - expect(res.status).toBe(200); - expect(mockDispatcher.handleAuth).toHaveBeenCalled(); - }); - - it('forwards GET requests to auth service', async () => { - const mockHandleRequest = vi.fn().mockResolvedValue( - new Response(JSON.stringify({ session: { token: 'abc' } }), { - status: 200, - headers: { 'Content-Type': 'application/json' }, - }), - ); - const kernelWithAuth = { - ...mockKernel, - getService: vi.fn().mockReturnValue({ handleRequest: mockHandleRequest }), - }; - const app = createHonoApp({ kernel: kernelWithAuth }); - const res = await app.request('/api/auth/get-session', { method: 'GET' }); - expect(res.status).toBe(200); - const json = await res.json(); - expect(json.session.token).toBe('abc'); - expect(mockHandleRequest).toHaveBeenCalled(); - }); - - it('returns error when auth service throws', async () => { - const mockHandleRequest = vi.fn().mockRejectedValue(new Error('Auth failed')); - const kernelWithAuth = { - ...mockKernel, - getService: vi.fn().mockReturnValue({ handleRequest: mockHandleRequest }), - }; - const app = createHonoApp({ kernel: kernelWithAuth }); - const res = await app.request('/api/auth/sign-in/email', { method: 'POST' }); - expect(res.status).toBe(500); - const json = await res.json(); - expect(json.success).toBe(false); - expect(json.error.message).toBe('Auth failed'); - }); - }); - - describe('GraphQL Endpoint', () => { - it('POST /api/graphql calls handleGraphQL', async () => { - const app = createHonoApp({ kernel: mockKernel }); - const body = { query: '{ objects { name } }' }; - const res = await app.request('/api/graphql', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(body), - }); - expect(res.status).toBe(200); - expect(mockDispatcher.handleGraphQL).toHaveBeenCalledWith( - body, - expect.objectContaining({ request: expect.any(Request) }), - ); - }); - - it('returns error on handleGraphQL exception', async () => { - mockDispatcher.handleGraphQL.mockRejectedValueOnce(new Error('Parse error')); - const app = createHonoApp({ kernel: mockKernel }); - const res = await app.request('/api/graphql', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ query: 'bad' }), - }); - expect(res.status).toBe(500); - const json = await res.json(); - expect(json.success).toBe(false); - }); - }); - - describe('Metadata Endpoint', () => { - it('GET /api/meta/objects calls handleMetadata', async () => { - const app = createHonoApp({ kernel: mockKernel }); - const res = await app.request('/api/meta/objects'); - expect(res.status).toBe(200); - expect(mockDispatcher.handleMetadata).toHaveBeenCalledWith( - '/objects', - expect.objectContaining({ request: expect.any(Request) }), - 'GET', - undefined, - expect.any(Object), - ); - }); - - it('PUT /api/meta/objects parses JSON body', async () => { - const app = createHonoApp({ kernel: mockKernel }); - const body = { name: 'test_object' }; - const res = await app.request('/api/meta/objects', { - method: 'PUT', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(body), - }); - expect(res.status).toBe(200); - expect(mockDispatcher.handleMetadata).toHaveBeenCalledWith( - '/objects', - expect.objectContaining({ request: expect.any(Request) }), - 'PUT', - body, - expect.any(Object), - ); - }); - }); - - describe('Data Endpoint', () => { - it('GET /api/data/account calls handleData', async () => { - const app = createHonoApp({ kernel: mockKernel }); - const res = await app.request('/api/data/account'); - expect(res.status).toBe(200); - expect(mockDispatcher.handleData).toHaveBeenCalledWith( - '/account', - 'GET', - {}, - expect.any(Object), - expect.objectContaining({ request: expect.any(Request) }), - ); - }); - - it('POST /api/data/account parses JSON body', async () => { - const app = createHonoApp({ kernel: mockKernel }); - const body = { name: 'Acme' }; - const res = await app.request('/api/data/account', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(body), - }); - expect(res.status).toBe(200); - expect(mockDispatcher.handleData).toHaveBeenCalledWith( - '/account', - 'POST', - body, - expect.any(Object), - expect.objectContaining({ request: expect.any(Request) }), - ); - }); - - it('returns 404 when result is not handled', async () => { - mockDispatcher.handleData.mockResolvedValueOnce({ handled: false }); - const app = createHonoApp({ kernel: mockKernel }); - const res = await app.request('/api/data/missing'); - expect(res.status).toBe(404); - const json = await res.json(); - expect(json.success).toBe(false); - }); - }); - - describe('Analytics Endpoint', () => { - it('GET /api/analytics/report calls handleAnalytics', async () => { - const app = createHonoApp({ kernel: mockKernel }); - const res = await app.request('/api/analytics/report'); - expect(res.status).toBe(200); - expect(mockDispatcher.handleAnalytics).toHaveBeenCalled(); - }); - - it('POST /api/analytics/report parses body', async () => { - const app = createHonoApp({ kernel: mockKernel }); - const body = { metric: 'revenue' }; - const res = await app.request('/api/analytics/report', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(body), - }); - expect(res.status).toBe(200); - expect(mockDispatcher.handleAnalytics).toHaveBeenCalledWith( - '/report', - 'POST', - body, - expect.objectContaining({ request: expect.any(Request) }), - ); - }); - }); - - describe('Automation Endpoint', () => { - it('GET /api/automation/flows calls handleAutomation', async () => { - const app = createHonoApp({ kernel: mockKernel }); - const res = await app.request('/api/automation/flows'); - expect(res.status).toBe(200); - expect(mockDispatcher.handleAutomation).toHaveBeenCalled(); - }); - - it('POST /api/automation/flows parses body', async () => { - const app = createHonoApp({ kernel: mockKernel }); - const body = { trigger: 'on_create' }; - const res = await app.request('/api/automation/flows', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(body), - }); - expect(res.status).toBe(200); - expect(mockDispatcher.handleAutomation).toHaveBeenCalledWith( - '/flows', - 'POST', - body, - expect.objectContaining({ request: expect.any(Request) }), - ); - }); - }); - - describe('Storage Endpoint', () => { - it('GET /api/storage/files calls handleStorage', async () => { - const app = createHonoApp({ kernel: mockKernel }); - const res = await app.request('/api/storage/files'); - expect(res.status).toBe(200); - expect(mockDispatcher.handleStorage).toHaveBeenCalled(); - }); - }); - - describe('Packages Endpoint', () => { - it('GET /api/packages calls handlePackages', async () => { - const app = createHonoApp({ kernel: mockKernel }); - const res = await app.request('/api/packages'); - expect(res.status).toBe(200); - expect(mockDispatcher.handlePackages).toHaveBeenCalledWith( - '', - 'GET', - {}, - expect.any(Object), - expect.objectContaining({ request: expect.any(Request) }), - ); - }); - - it('POST /api/packages/install parses body', async () => { - const app = createHonoApp({ kernel: mockKernel }); - const body = { package: 'my-plugin' }; - const res = await app.request('/api/packages/install', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(body), - }); - expect(res.status).toBe(200); - expect(mockDispatcher.handlePackages).toHaveBeenCalledWith( - '/install', - 'POST', - body, - expect.any(Object), - expect.objectContaining({ request: expect.any(Request) }), - ); - }); - }); - - describe('normalizeResponse', () => { - it('handles redirect result', async () => { - mockDispatcher.handleData.mockResolvedValueOnce({ - handled: true, - result: { type: 'redirect', url: 'https://example.com' }, - }); - const app = createHonoApp({ kernel: mockKernel }); - const res = await app.request('/api/data/redir', { redirect: 'manual' }); - expect(res.status).toBe(302); - expect(res.headers.get('location')).toBe('https://example.com'); - }); - - it('handles stream result', async () => { - const stream = new ReadableStream({ - start(controller) { - controller.enqueue(new TextEncoder().encode('hello')); - controller.close(); - }, - }); - mockDispatcher.handleData.mockResolvedValueOnce({ - handled: true, - result: { type: 'stream', stream, headers: { 'Content-Type': 'text/plain' } }, - }); - const app = createHonoApp({ kernel: mockKernel }); - const res = await app.request('/api/data/stream'); - expect(res.status).toBe(200); - const text = await res.text(); - expect(text).toBe('hello'); - }); - }); -}); - -describe('objectStackMiddleware', () => { it('sets kernel on context via c.set', async () => { const app = new Hono(); const middleware = objectStackMiddleware(mockKernel); @@ -429,4 +44,20 @@ describe('objectStackMiddleware', () => { expect(res.status).toBe(200); expect(spy).toHaveBeenCalled(); }); + + it('provides the correct kernel instance', async () => { + const app = new Hono(); + const middleware = objectStackMiddleware(mockKernel); + + app.use('*', middleware); + app.get('/test', (c) => { + const kernel = c.get('objectStack'); + return c.json({ name: kernel.name }); + }); + + const res = await app.request('/test'); + expect(res.status).toBe(200); + const json = await res.json(); + expect(json.name).toBe('test-kernel'); + }); }); diff --git a/packages/adapters/hono/src/index.ts b/packages/adapters/hono/src/index.ts index 63dbd4664a..13497d4a42 100644 --- a/packages/adapters/hono/src/index.ts +++ b/packages/adapters/hono/src/index.ts @@ -1,232 +1,12 @@ // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. -import { Hono } from 'hono'; -import { cors } from 'hono/cors'; -import { type ObjectKernel, HttpDispatcher, HttpDispatcherResult } from '@objectstack/runtime'; +import { type ObjectKernel } from '@objectstack/runtime'; export interface ObjectStackHonoOptions { kernel: ObjectKernel; prefix?: string; } -/** - * Auth service interface with handleRequest method - */ -interface AuthService { - handleRequest(request: Request): Promise; -} - -/** - * @deprecated Use `HonoServerPlugin` + `createRestApiPlugin()` + `createDispatcherPlugin()` instead. - * This function bundles all routes into a single Hono app using the legacy HttpDispatcher. - * The plugin-based approach provides better modularity and separation of concerns. - * - * Migration: - * ```ts - * // Before: - * const app = createHonoApp({ kernel, prefix: '/api/v1' }); - * - * // After: - * import { createRestApiPlugin } from '@objectstack/rest'; - * import { createDispatcherPlugin } from '@objectstack/runtime'; - * kernel.use(new HonoServerPlugin({ port: 3000 })); - * kernel.use(createRestApiPlugin()); - * kernel.use(createDispatcherPlugin({ prefix: '/api/v1' })); - * ``` - */ -export function createHonoApp(options: ObjectStackHonoOptions) { - const app = new Hono(); - const { prefix = '/api' } = options; - const dispatcher = new HttpDispatcher(options.kernel); - - app.use('*', cors()); - - // --- Helper for Response Normalization --- - const normalizeResponse = (c: any, result: HttpDispatcherResult) => { - if (result.handled) { - if (result.response) { - return c.json(result.response.body, result.response.status as any, result.response.headers); - } - if (result.result) { - const res = result.result; - // Redirect - if (res.type === 'redirect' && res.url) { - return c.redirect(res.url); - } - // Stream - if (res.type === 'stream' && res.stream) { - return c.body(res.stream, 200, res.headers); - } - - // Hono handles standard Response objects - return res; - } - } - return c.json({ success: false, error: { message: 'Not Found', code: 404 } }, 404); - } - - // --- 0. Discovery Endpoint --- - app.get(prefix, (c) => { - return c.json({ data: dispatcher.getDiscoveryInfo(prefix) }); - }); - - // --- 1. Auth --- - app.all(`${prefix}/auth/*`, async (c) => { - try { - // Try AuthPlugin service first (preferred path) - const authService = typeof options.kernel.getService === 'function' - ? options.kernel.getService('auth') - : null; - - if (authService && typeof authService.handleRequest === 'function') { - const response = await authService.handleRequest(c.req.raw); - return response; - } - - // Fallback to legacy dispatcher - const path = c.req.path.substring(c.req.path.indexOf('/auth/') + 6); - const body = await c.req.parseBody().catch(() => ({})); - - const result = await dispatcher.handleAuth(path, c.req.method, body, { request: c.req.raw }); - return normalizeResponse(c, result); - } catch (err: any) { - return c.json({ success: false, error: { message: err.message, code: err.statusCode || 500 } }, err.statusCode || 500); - } - }); - - // --- 2. GraphQL --- - app.post(`${prefix}/graphql`, async (c) => { - try { - const body = await c.req.json(); - const result = await dispatcher.handleGraphQL(body, { request: c.req.raw }); - return c.json(result); - } catch (err: any) { - return c.json({ success: false, error: { message: err.message, code: err.statusCode || 500 } }, err.statusCode || 500); - } - }); - - // --- 3. Metadata Endpoints --- - app.all(`${prefix}/meta*`, async (c) => { - try { - const path = c.req.path.substring(c.req.path.indexOf('/meta') + 5); - const method = c.req.method; - let body = undefined; - - if (method === 'PUT' || method === 'POST') { - // Attempt to parse JSON body - try { - body = await c.req.json(); - } catch (e) { - // Ignore parse errors, body remains undefined or empty - body = {}; - } - } - const query = c.req.query(); - - const result = await dispatcher.handleMetadata(path, { request: c.req.raw }, method, body, query); - return normalizeResponse(c, result); - } catch (err: any) { - return c.json({ success: false, error: { message: err.message, code: err.statusCode || 500 } }, err.statusCode || 500); - } - }); - - // --- 4. Data Endpoints --- - app.all(`${prefix}/data*`, async (c) => { - try { - const path = c.req.path.substring(c.req.path.indexOf('/data') + 5); - const method = c.req.method; - - let body = {}; - if (method === 'POST' || method === 'PATCH') { - body = await c.req.json().catch(() => ({})); - } - const query = c.req.query(); - - const result = await dispatcher.handleData(path, method, body, query, { request: c.req.raw }); - return normalizeResponse(c, result); - } catch (err: any) { - return c.json({ success: false, error: { message: err.message, code: err.statusCode || 500 } }, err.statusCode || 500); - } - }); - - // --- 5. Analytics Endpoints --- - app.all(`${prefix}/analytics*`, async (c) => { - try { - const path = c.req.path.substring(c.req.path.indexOf('/analytics') + 10); - const method = c.req.method; - - let body = {}; - if (method === 'POST') { - body = await c.req.json().catch(() => ({})); - } - - const result = await dispatcher.handleAnalytics(path, method, body, { request: c.req.raw }); - return normalizeResponse(c, result); - } catch (err: any) { - return c.json({ success: false, error: { message: err.message, code: err.statusCode || 500 } }, err.statusCode || 500); - } - }); - - // --- 7. Automation Endpoints --- - app.all(`${prefix}/automation*`, async (c) => { - try { - const path = c.req.path.substring(c.req.path.indexOf('/automation') + 11); - const method = c.req.method; - - let body = {}; - if (method === 'POST') { - body = await c.req.json().catch(() => ({})); - } - - const result = await dispatcher.handleAutomation(path, method, body, { request: c.req.raw }); - return normalizeResponse(c, result); - } catch (err: any) { - return c.json({ success: false, error: { message: err.message, code: err.statusCode || 500 } }, err.statusCode || 500); - } - }); - - // --- 8. Storage Endpoints --- - app.all(`${prefix}/storage*`, async (c) => { - try { - const path = c.req.path.substring(c.req.path.indexOf('/storage') + 8); - const method = c.req.method; - - let file: any = undefined; - if (method === 'POST' && path.includes('upload')) { - const body = await c.req.parseBody(); - file = body['file']; - } - - const result = await dispatcher.handleStorage(path, method, file, { request: c.req.raw }); - return normalizeResponse(c, result); - } catch (err: any) { - return c.json({ success: false, error: { message: err.message, code: err.statusCode || 500 } }, err.statusCode || 500); - } - }); - - // --- 9. Package Management Endpoints --- - app.all(`${prefix}/packages*`, async (c) => { - try { - const packagesIndex = c.req.path.indexOf('/packages'); - const path = c.req.path.substring(packagesIndex + 9); // length of '/packages' - const method = c.req.method; - - let body = {}; - if (method === 'POST' || method === 'PATCH' || method === 'PUT') { - body = await c.req.json().catch(() => ({})); - } - const query = c.req.query(); - - const result = await dispatcher.handlePackages(path, method, body, query, { request: c.req.raw }); - return normalizeResponse(c, result); - } catch (err: any) { - return c.json({ success: false, error: { message: err.message, code: err.statusCode || 500 } }, err.statusCode || 500); - } - }); - - return app; -} - /** * Middleware mode for existing Hono apps */ diff --git a/packages/adapters/hono/src/metadata-api.test.ts b/packages/adapters/hono/src/metadata-api.test.ts deleted file mode 100644 index 911d94f7eb..0000000000 --- a/packages/adapters/hono/src/metadata-api.test.ts +++ /dev/null @@ -1,852 +0,0 @@ -// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. - -/** - * @objectstack/hono — Comprehensive Metadata API Integration Tests - * - * Validates that the Hono adapter correctly routes ALL metadata API operations - * defined by the @objectstack/metadata package through the HttpDispatcher. - * - * Covers: CRUD, Query, Bulk, Overlay, Import/Export, Validation, Type Registry, Dependencies - */ - -import { describe, it, expect, vi, beforeEach } from 'vitest'; -import { Hono } from 'hono'; - -// Mock dispatcher instance with all metadata-related handlers -const mockDispatcher = { - getDiscoveryInfo: vi.fn().mockReturnValue({ version: '1.0', endpoints: [] }), - handleAuth: vi.fn().mockResolvedValue({ handled: true, response: { body: { ok: true }, status: 200 } }), - handleGraphQL: vi.fn().mockResolvedValue({ data: {} }), - handleMetadata: vi.fn().mockResolvedValue({ handled: true, response: { body: { success: true }, status: 200 } }), - handleData: vi.fn().mockResolvedValue({ handled: true, response: { body: { records: [] }, status: 200 } }), - handleAnalytics: vi.fn().mockResolvedValue({ handled: true, response: { body: {}, status: 200 } }), - handleAutomation: vi.fn().mockResolvedValue({ handled: true, response: { body: {}, status: 200 } }), - handleStorage: vi.fn().mockResolvedValue({ handled: true, response: { body: {}, status: 200 } }), - handlePackages: vi.fn().mockResolvedValue({ handled: true, response: { body: {}, status: 200 } }), -}; - -vi.mock('@objectstack/runtime', () => { - return { - HttpDispatcher: function HttpDispatcher() { - return mockDispatcher; - }, - }; -}); - -import { createHonoApp } from './index'; - -const mockKernel = { name: 'test-kernel' } as any; - -describe('Hono Metadata API Integration Tests', () => { - let app: ReturnType; - - beforeEach(() => { - vi.clearAllMocks(); - app = createHonoApp({ kernel: mockKernel }); - }); - - // ========================================== - // CRUD Operations - // ========================================== - - describe('CRUD Operations', () => { - describe('GET /api/meta/objects — List all objects', () => { - it('dispatches to handleMetadata with correct path and method', async () => { - mockDispatcher.handleMetadata.mockResolvedValueOnce({ - handled: true, - response: { - body: { - success: true, - data: [ - { name: 'account', label: 'Account' }, - { name: 'contact', label: 'Contact' }, - ], - }, - status: 200, - }, - }); - - const res = await app.request('/api/meta/objects'); - expect(res.status).toBe(200); - const json = await res.json(); - expect(json.success).toBe(true); - expect(json.data).toHaveLength(2); - expect(mockDispatcher.handleMetadata).toHaveBeenCalledWith( - '/objects', - expect.objectContaining({ request: expect.any(Request) }), - 'GET', - undefined, - expect.any(Object), - ); - }); - }); - - describe('GET /api/meta/objects/account — Get single object', () => { - it('dispatches to handleMetadata with item-level path', async () => { - mockDispatcher.handleMetadata.mockResolvedValueOnce({ - handled: true, - response: { - body: { - success: true, - data: { type: 'object', name: 'account', definition: { label: 'Account' } }, - }, - status: 200, - }, - }); - - const res = await app.request('/api/meta/objects/account'); - expect(res.status).toBe(200); - const json = await res.json(); - expect(json.data.name).toBe('account'); - expect(mockDispatcher.handleMetadata).toHaveBeenCalledWith( - '/objects/account', - expect.objectContaining({ request: expect.any(Request) }), - 'GET', - undefined, - expect.any(Object), - ); - }); - }); - - describe('POST /api/meta/objects — Register metadata', () => { - it('dispatches POST with JSON body to handleMetadata', async () => { - const body = { - type: 'object', - name: 'project_task', - data: { label: 'Project Task', fields: {} }, - }; - - mockDispatcher.handleMetadata.mockResolvedValueOnce({ - handled: true, - response: { body: { success: true }, status: 201 }, - }); - - const res = await app.request('/api/meta/objects', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(body), - }); - expect(res.status).toBe(201); - expect(mockDispatcher.handleMetadata).toHaveBeenCalledWith( - '/objects', - expect.objectContaining({ request: expect.any(Request) }), - 'POST', - body, - expect.any(Object), - ); - }); - }); - - describe('PUT /api/meta/objects/account — Update metadata', () => { - it('dispatches PUT with JSON body to handleMetadata', async () => { - const body = { label: 'Updated Account' }; - - mockDispatcher.handleMetadata.mockResolvedValueOnce({ - handled: true, - response: { body: { success: true }, status: 200 }, - }); - - const res = await app.request('/api/meta/objects/account', { - method: 'PUT', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(body), - }); - expect(res.status).toBe(200); - expect(mockDispatcher.handleMetadata).toHaveBeenCalledWith( - '/objects/account', - expect.objectContaining({ request: expect.any(Request) }), - 'PUT', - body, - expect.any(Object), - ); - }); - }); - - describe('DELETE /api/meta/objects/old_entity — Delete metadata', () => { - it('dispatches DELETE to handleMetadata', async () => { - mockDispatcher.handleMetadata.mockResolvedValueOnce({ - handled: true, - response: { - body: { success: true, data: { type: 'object', name: 'old_entity' } }, - status: 200, - }, - }); - - const res = await app.request('/api/meta/objects/old_entity', { method: 'DELETE' }); - expect(res.status).toBe(200); - const json = await res.json(); - expect(json.data.name).toBe('old_entity'); - }); - }); - - describe('GET /api/meta/objects/account/exists — Check existence', () => { - it('dispatches existence check', async () => { - mockDispatcher.handleMetadata.mockResolvedValueOnce({ - handled: true, - response: { - body: { success: true, data: { exists: true } }, - status: 200, - }, - }); - - const res = await app.request('/api/meta/objects/account/exists'); - expect(res.status).toBe(200); - const json = await res.json(); - expect(json.data.exists).toBe(true); - }); - }); - - describe('GET /api/meta/objects/names — List names', () => { - it('dispatches names listing', async () => { - mockDispatcher.handleMetadata.mockResolvedValueOnce({ - handled: true, - response: { - body: { success: true, data: ['account', 'contact', 'lead'] }, - status: 200, - }, - }); - - const res = await app.request('/api/meta/objects/names'); - expect(res.status).toBe(200); - const json = await res.json(); - expect(json.data).toHaveLength(3); - }); - }); - - describe('Multiple metadata types', () => { - it('GET /api/meta/views dispatches for views', async () => { - const res = await app.request('/api/meta/views'); - expect(mockDispatcher.handleMetadata).toHaveBeenCalledWith( - '/views', - expect.objectContaining({ request: expect.any(Request) }), - 'GET', - undefined, - expect.any(Object), - ); - }); - - it('GET /api/meta/flows dispatches for flows', async () => { - const res = await app.request('/api/meta/flows'); - expect(mockDispatcher.handleMetadata).toHaveBeenCalledWith( - '/flows', - expect.objectContaining({ request: expect.any(Request) }), - 'GET', - undefined, - expect.any(Object), - ); - }); - - it('GET /api/meta/agents dispatches for agents', async () => { - const res = await app.request('/api/meta/agents'); - expect(mockDispatcher.handleMetadata).toHaveBeenCalledWith( - '/agents', - expect.objectContaining({ request: expect.any(Request) }), - 'GET', - undefined, - expect.any(Object), - ); - }); - }); - }); - - // ========================================== - // Query / Search - // ========================================== - - describe('Query / Search', () => { - describe('POST /api/meta/query — Advanced search', () => { - it('dispatches query with full filter payload', async () => { - const queryBody = { - types: ['object', 'view'], - search: 'account', - scope: 'platform', - state: 'active', - sortBy: 'updatedAt', - sortOrder: 'desc', - page: 1, - pageSize: 25, - }; - - mockDispatcher.handleMetadata.mockResolvedValueOnce({ - handled: true, - response: { - body: { - success: true, - data: { - items: [{ type: 'object', name: 'account', label: 'Account' }], - total: 1, - page: 1, - pageSize: 25, - }, - }, - status: 200, - }, - }); - - const res = await app.request('/api/meta/query', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(queryBody), - }); - - expect(res.status).toBe(200); - const json = await res.json(); - expect(json.data.items).toHaveLength(1); - expect(json.data.total).toBe(1); - }); - }); - }); - - // ========================================== - // Bulk Operations - // ========================================== - - describe('Bulk Operations', () => { - describe('POST /api/meta/bulk/register — Bulk register', () => { - it('dispatches bulk register with items', async () => { - const bulkBody = { - items: [ - { type: 'object', name: 'customer', data: { label: 'Customer' } }, - { type: 'view', name: 'customer_list', data: { label: 'Customer List' } }, - ], - continueOnError: true, - }; - - mockDispatcher.handleMetadata.mockResolvedValueOnce({ - handled: true, - response: { - body: { - success: true, - data: { total: 2, succeeded: 2, failed: 0 }, - }, - status: 200, - }, - }); - - const res = await app.request('/api/meta/bulk/register', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(bulkBody), - }); - - expect(res.status).toBe(200); - const json = await res.json(); - expect(json.data.succeeded).toBe(2); - }); - }); - - describe('POST /api/meta/bulk/unregister — Bulk unregister', () => { - it('dispatches bulk unregister', async () => { - const body = { - items: [ - { type: 'object', name: 'old_object' }, - { type: 'view', name: 'old_view' }, - ], - }; - - mockDispatcher.handleMetadata.mockResolvedValueOnce({ - handled: true, - response: { - body: { success: true, data: { total: 2, succeeded: 2, failed: 0 } }, - status: 200, - }, - }); - - const res = await app.request('/api/meta/bulk/unregister', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(body), - }); - - expect(res.status).toBe(200); - const json = await res.json(); - expect(json.data.succeeded).toBe(2); - }); - }); - - describe('Bulk operation with partial failures', () => { - it('returns error details for failed items', async () => { - mockDispatcher.handleMetadata.mockResolvedValueOnce({ - handled: true, - response: { - body: { - success: true, - data: { - total: 3, - succeeded: 2, - failed: 1, - errors: [{ type: 'object', name: 'bad_item', error: 'Validation failed' }], - }, - }, - status: 200, - }, - }); - - const res = await app.request('/api/meta/bulk/register', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - items: [ - { type: 'object', name: 'good', data: {} }, - { type: 'object', name: 'good2', data: {} }, - { type: 'object', name: 'bad_item', data: {} }, - ], - continueOnError: true, - }), - }); - - expect(res.status).toBe(200); - const json = await res.json(); - expect(json.data.failed).toBe(1); - expect(json.data.errors[0].name).toBe('bad_item'); - }); - }); - }); - - // ========================================== - // Overlay / Customization - // ========================================== - - describe('Overlay / Customization', () => { - describe('GET /api/meta/objects/account/overlay — Get overlay', () => { - it('dispatches overlay retrieval', async () => { - mockDispatcher.handleMetadata.mockResolvedValueOnce({ - handled: true, - response: { - body: { - success: true, - data: { - id: 'overlay-001', - baseType: 'object', - baseName: 'account', - scope: 'platform', - patch: { fields: { status: { label: 'Custom Status' } } }, - }, - }, - status: 200, - }, - }); - - const res = await app.request('/api/meta/objects/account/overlay'); - expect(res.status).toBe(200); - const json = await res.json(); - expect(json.data.scope).toBe('platform'); - }); - }); - - describe('PUT /api/meta/objects/account/overlay — Save overlay', () => { - it('dispatches overlay save with body', async () => { - const overlayBody = { - id: 'overlay-002', - baseType: 'object', - baseName: 'account', - scope: 'platform', - patch: { fields: { status: { label: 'Account Status' } } }, - }; - - mockDispatcher.handleMetadata.mockResolvedValueOnce({ - handled: true, - response: { body: { success: true }, status: 200 }, - }); - - const res = await app.request('/api/meta/objects/account/overlay', { - method: 'PUT', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(overlayBody), - }); - - expect(res.status).toBe(200); - expect(mockDispatcher.handleMetadata).toHaveBeenCalledWith( - '/objects/account/overlay', - expect.objectContaining({ request: expect.any(Request) }), - 'PUT', - overlayBody, - expect.any(Object), - ); - }); - }); - - describe('DELETE /api/meta/objects/account/overlay — Remove overlay', () => { - it('dispatches overlay removal', async () => { - mockDispatcher.handleMetadata.mockResolvedValueOnce({ - handled: true, - response: { body: { success: true }, status: 200 }, - }); - - const res = await app.request('/api/meta/objects/account/overlay', { method: 'DELETE' }); - expect(res.status).toBe(200); - }); - }); - - describe('GET /api/meta/objects/account/effective — Get effective metadata', () => { - it('dispatches effective (merged) metadata retrieval', async () => { - mockDispatcher.handleMetadata.mockResolvedValueOnce({ - handled: true, - response: { - body: { - success: true, - data: { - name: 'account', - label: 'Account', - fields: { status: { label: 'Custom Status' } }, - }, - }, - status: 200, - }, - }); - - const res = await app.request('/api/meta/objects/account/effective'); - expect(res.status).toBe(200); - const json = await res.json(); - expect(json.data.fields.status.label).toBe('Custom Status'); - }); - }); - }); - - // ========================================== - // Import / Export - // ========================================== - - describe('Import / Export', () => { - describe('POST /api/meta/export — Export metadata', () => { - it('dispatches export request', async () => { - mockDispatcher.handleMetadata.mockResolvedValueOnce({ - handled: true, - response: { - body: { - success: true, - data: { version: '1.0', objects: { account: {} }, views: {} }, - }, - status: 200, - }, - }); - - const res = await app.request('/api/meta/export', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ types: ['object', 'view'], format: 'json' }), - }); - - expect(res.status).toBe(200); - const json = await res.json(); - expect(json.data.version).toBe('1.0'); - }); - }); - - describe('POST /api/meta/import — Import metadata', () => { - it('dispatches import request', async () => { - mockDispatcher.handleMetadata.mockResolvedValueOnce({ - handled: true, - response: { - body: { - success: true, - data: { total: 5, imported: 4, skipped: 1, failed: 0 }, - }, - status: 200, - }, - }); - - const res = await app.request('/api/meta/import', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - data: { objects: { customer: {} } }, - conflictResolution: 'merge', - validate: true, - }), - }); - - expect(res.status).toBe(200); - const json = await res.json(); - expect(json.data.imported).toBe(4); - expect(json.data.skipped).toBe(1); - }); - }); - - describe('POST /api/meta/import — Dry run', () => { - it('returns import preview without saving', async () => { - mockDispatcher.handleMetadata.mockResolvedValueOnce({ - handled: true, - response: { - body: { - success: true, - data: { total: 3, imported: 0, skipped: 0, failed: 0 }, - }, - status: 200, - }, - }); - - const res = await app.request('/api/meta/import', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ data: {}, dryRun: true }), - }); - - expect(res.status).toBe(200); - }); - }); - }); - - // ========================================== - // Validation - // ========================================== - - describe('Validation', () => { - describe('POST /api/meta/validate — Validate metadata', () => { - it('dispatches validation for valid payload', async () => { - mockDispatcher.handleMetadata.mockResolvedValueOnce({ - handled: true, - response: { - body: { success: true, data: { valid: true } }, - status: 200, - }, - }); - - const res = await app.request('/api/meta/validate', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ type: 'object', data: { name: 'test', fields: {} } }), - }); - - expect(res.status).toBe(200); - const json = await res.json(); - expect(json.data.valid).toBe(true); - }); - - it('returns errors for invalid payload', async () => { - mockDispatcher.handleMetadata.mockResolvedValueOnce({ - handled: true, - response: { - body: { - success: true, - data: { - valid: false, - errors: [{ path: 'name', message: 'Required', code: 'required' }], - }, - }, - status: 200, - }, - }); - - const res = await app.request('/api/meta/validate', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ type: 'object', data: {} }), - }); - - expect(res.status).toBe(200); - const json = await res.json(); - expect(json.data.valid).toBe(false); - expect(json.data.errors).toHaveLength(1); - }); - }); - }); - - // ========================================== - // Type Registry - // ========================================== - - describe('Type Registry', () => { - describe('GET /api/meta/types — List registered types', () => { - it('returns all registered metadata types', async () => { - mockDispatcher.handleMetadata.mockResolvedValueOnce({ - handled: true, - response: { - body: { - success: true, - data: ['object', 'field', 'view', 'page', 'dashboard', 'app', 'flow', 'agent'], - }, - status: 200, - }, - }); - - const res = await app.request('/api/meta/types'); - expect(res.status).toBe(200); - const json = await res.json(); - expect(json.data).toContain('object'); - expect(json.data).toContain('agent'); - }); - }); - - describe('GET /api/meta/types/object — Get type info', () => { - it('returns type metadata details', async () => { - mockDispatcher.handleMetadata.mockResolvedValueOnce({ - handled: true, - response: { - body: { - success: true, - data: { - type: 'object', - label: 'Object', - description: 'Business entity definition', - filePatterns: ['**/*.object.ts'], - supportsOverlay: true, - domain: 'data', - }, - }, - status: 200, - }, - }); - - const res = await app.request('/api/meta/types/object'); - expect(res.status).toBe(200); - const json = await res.json(); - expect(json.data.domain).toBe('data'); - expect(json.data.supportsOverlay).toBe(true); - }); - }); - }); - - // ========================================== - // Dependency Tracking - // ========================================== - - describe('Dependency Tracking', () => { - describe('GET /api/meta/objects/account/dependencies — Get dependencies', () => { - it('returns items this item depends on', async () => { - mockDispatcher.handleMetadata.mockResolvedValueOnce({ - handled: true, - response: { - body: { - success: true, - data: [ - { - sourceType: 'object', - sourceName: 'account', - targetType: 'object', - targetName: 'organization', - kind: 'reference', - }, - ], - }, - status: 200, - }, - }); - - const res = await app.request('/api/meta/objects/account/dependencies'); - expect(res.status).toBe(200); - const json = await res.json(); - expect(json.data).toHaveLength(1); - expect(json.data[0].kind).toBe('reference'); - }); - }); - - describe('GET /api/meta/objects/account/dependents — Get dependents', () => { - it('returns items that depend on this item', async () => { - mockDispatcher.handleMetadata.mockResolvedValueOnce({ - handled: true, - response: { - body: { - success: true, - data: [ - { - sourceType: 'view', - sourceName: 'account_list', - targetType: 'object', - targetName: 'account', - kind: 'reference', - }, - { - sourceType: 'flow', - sourceName: 'new_account_flow', - targetType: 'object', - targetName: 'account', - kind: 'triggers', - }, - ], - }, - status: 200, - }, - }); - - const res = await app.request('/api/meta/objects/account/dependents'); - expect(res.status).toBe(200); - const json = await res.json(); - expect(json.data).toHaveLength(2); - }); - }); - }); - - // ========================================== - // Error Handling - // ========================================== - - describe('Error Handling', () => { - it('returns 404 when metadata not found', async () => { - mockDispatcher.handleMetadata.mockResolvedValueOnce({ handled: false }); - - const res = await app.request('/api/meta/objects/nonexistent'); - expect(res.status).toBe(404); - const json = await res.json(); - expect(json.success).toBe(false); - }); - - it('returns 500 on dispatcher exception', async () => { - mockDispatcher.handleMetadata.mockRejectedValueOnce(new Error('Internal error')); - - const res = await app.request('/api/meta/objects'); - expect(res.status).toBe(500); - const json = await res.json(); - expect(json.success).toBe(false); - expect(json.error.message).toBe('Internal error'); - }); - - it('returns custom status code from error', async () => { - mockDispatcher.handleMetadata.mockRejectedValueOnce( - Object.assign(new Error('Forbidden'), { statusCode: 403 }), - ); - - const res = await app.request('/api/meta/objects'); - expect(res.status).toBe(403); - }); - - it('handles malformed JSON body gracefully', async () => { - const res = await app.request('/api/meta/objects', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: 'not valid json', - }); - // Should still route to handleMetadata with empty body fallback - expect(mockDispatcher.handleMetadata).toHaveBeenCalled(); - }); - }); - - // ========================================== - // Path Parsing - // ========================================== - - describe('Path Parsing', () => { - it('correctly parses nested paths', async () => { - await app.request('/api/meta/objects/account/fields/name'); - expect(mockDispatcher.handleMetadata).toHaveBeenCalledWith( - '/objects/account/fields/name', - expect.any(Object), - 'GET', - undefined, - expect.any(Object), - ); - }); - - it('correctly parses path with query string', async () => { - await app.request('/api/meta/objects?scope=platform&state=active'); - expect(mockDispatcher.handleMetadata).toHaveBeenCalledWith( - '/objects', - expect.any(Object), - 'GET', - undefined, - expect.objectContaining({ scope: 'platform', state: 'active' }), - ); - }); - - it('handles root metadata path', async () => { - await app.request('/api/meta'); - expect(mockDispatcher.handleMetadata).toHaveBeenCalledWith( - '', - expect.any(Object), - 'GET', - undefined, - expect.any(Object), - ); - }); - }); -}); diff --git a/packages/client/src/index.ts b/packages/client/src/index.ts index a41ff576f5..86ef729e37 100644 --- a/packages/client/src/index.ts +++ b/packages/client/src/index.ts @@ -104,12 +104,8 @@ export interface QueryOptions { } export interface PaginatedResult { - /** @deprecated Use `records` — aligned with FindDataResponseSchema */ - value?: T[]; /** Spec-compliant: array of matching records */ records: T[]; - /** @deprecated Use `total` — aligned with FindDataResponseSchema */ - count?: number; /** Total number of matching records (if requested) */ total?: number; /** The object name */ @@ -268,17 +264,6 @@ export class ObjectStackClient { return this.unwrapResponse(res); }, - /** - * Get a specific object definition by name - * @deprecated Use `getItem('object', name)` instead for consistency with spec protocol - * @param name - Object name (snake_case identifier) - */ - getObject: async (name: string) => { - const route = this.getRoute('metadata'); - const res = await this.fetch(`${this.baseUrl}${route}/object/${name}`); - return this.unwrapResponse(res); - }, - /** * Get a specific metadata item by type and name * @param type - Metadata type (e.g., 'object', 'plugin') diff --git a/packages/core/src/plugin-loader.ts b/packages/core/src/plugin-loader.ts index 5227d0bbd8..25e6c6baf1 100644 --- a/packages/core/src/plugin-loader.ts +++ b/packages/core/src/plugin-loader.ts @@ -34,16 +34,6 @@ export interface ServiceRegistration { dependencies?: string[]; } -/** - * Plugin Configuration Validator Interface - * Uses Zod for runtime validation of plugin configurations - * @deprecated Use the PluginConfigValidator class from security module instead - */ -export interface IPluginConfigValidator { - schema: z.ZodSchema; - validate(config: any): any; -} - /** * Plugin Metadata with Enhanced Features */ diff --git a/packages/rest/src/index.ts b/packages/rest/src/index.ts index 4cf04a2375..2450aca6e8 100644 --- a/packages/rest/src/index.ts +++ b/packages/rest/src/index.ts @@ -10,7 +10,3 @@ export type { RouteEntry } from './route-manager.js'; // REST API Plugin export { createRestApiPlugin } from './rest-api-plugin.js'; export type { RestApiPluginConfig } from './rest-api-plugin.js'; - -// Backward-compatible aliases (deprecated) -export { createApiRegistryPlugin } from './rest-api-plugin.js'; -export type { ApiRegistryConfig } from './rest-api-plugin.js'; diff --git a/packages/rest/src/rest-api-plugin.ts b/packages/rest/src/rest-api-plugin.ts index b3b215578f..e185a9bf64 100644 --- a/packages/rest/src/rest-api-plugin.ts +++ b/packages/rest/src/rest-api-plugin.ts @@ -10,11 +10,6 @@ export interface RestApiPluginConfig { api?: RestServerConfig; } -/** - * @deprecated Use {@link RestApiPluginConfig} instead - */ -export type ApiRegistryConfig = RestApiPluginConfig; - /** * REST API Plugin * @@ -75,8 +70,3 @@ export function createRestApiPlugin(config: RestApiPluginConfig = {}): Plugin { } }; } - -/** - * @deprecated Use {@link createRestApiPlugin} instead - */ -export const createApiRegistryPlugin = createRestApiPlugin; diff --git a/packages/rest/src/rest.test.ts b/packages/rest/src/rest.test.ts index 89a407ebdc..d297566293 100644 --- a/packages/rest/src/rest.test.ts +++ b/packages/rest/src/rest.test.ts @@ -3,7 +3,7 @@ import { describe, it, expect, beforeEach, vi } from 'vitest'; import { RouteManager, RouteGroupBuilder } from './route-manager'; import { RestServer } from './rest-server'; -import { createRestApiPlugin, createApiRegistryPlugin } from './rest-api-plugin'; +import { createRestApiPlugin } from './rest-api-plugin'; import type { RestApiPluginConfig } from './rest-api-plugin'; // --------------------------------------------------------------------------- @@ -520,13 +520,3 @@ describe('createRestApiPlugin', () => { }); }); }); - -// --------------------------------------------------------------------------- -// Backward-compatible aliases -// --------------------------------------------------------------------------- - -describe('backward-compatible aliases', () => { - it('createApiRegistryPlugin should be the same function as createRestApiPlugin', () => { - expect(createApiRegistryPlugin).toBe(createRestApiPlugin); - }); -}); diff --git a/packages/runtime/src/index.ts b/packages/runtime/src/index.ts index 5d39af255a..cbcd8e05a4 100644 --- a/packages/runtime/src/index.ts +++ b/packages/runtime/src/index.ts @@ -15,23 +15,20 @@ export type { DispatcherPluginConfig } from './dispatcher-plugin.js'; // Export HTTP Server Components export { HttpServer } from './http-server.js'; -/** @deprecated Use createDispatcherPlugin() instead. Will be removed in v3.0.0. */ export { HttpDispatcher } from './http-dispatcher.js'; export type { HttpProtocolContext, HttpDispatcherResult } from './http-dispatcher.js'; export { MiddlewareManager } from './middleware.js'; -// Re-export from @objectstack/rest for backward compatibility +// Re-export from @objectstack/rest export { RestServer, RouteManager, RouteGroupBuilder, createRestApiPlugin, - createApiRegistryPlugin, } from '@objectstack/rest'; export type { RouteEntry, RestApiPluginConfig, - ApiRegistryConfig, } from '@objectstack/rest'; // Export Types diff --git a/packages/spec/V3_MIGRATION_GUIDE.md b/packages/spec/V3_MIGRATION_GUIDE.md index 9726935f25..1f332c9918 100644 --- a/packages/spec/V3_MIGRATION_GUIDE.md +++ b/packages/spec/V3_MIGRATION_GUIDE.md @@ -146,14 +146,14 @@ import { EventBusConfigSchema, EventSchema, EventPriority } from '@objectstack/s ## Migration Checklist -- [ ] Replace all `Hub.*` imports with direct `system/` or `kernel/` imports -- [ ] Move `createErrorResponse()` / `getHttpStatusForCategory()` to `@objectstack/core/errors` -- [ ] Move `definePlugin()` to `@objectstack/core/plugin` -- [ ] Replace `location` with `locations` in ActionSchema definitions -- [ ] Replace `RealtimePresenceStatus` with `PresenceStatus` -- [ ] Replace `RealtimeAction` with `RealtimeRecordAction` -- [ ] Replace `RateLimitSchema` with `RateLimitConfigSchema` from `@objectstack/spec/shared` -- [ ] (No action needed) Events module restructuring is internal; imports from `@objectstack/spec/kernel` continue to work +- [x] Replace all `Hub.*` imports with direct `system/` or `kernel/` imports +- [x] Remove `createErrorResponse()` / `getHttpStatusForCategory()` from spec (moved to `@objectstack/core/errors`) +- [x] Remove `definePlugin()` from spec (moved to `@objectstack/core/plugin`) +- [x] Replace `location` with `locations` in ActionSchema definitions +- [x] Replace `RealtimePresenceStatus` with `PresenceStatus` +- [x] Replace `RealtimeAction` with `RealtimeRecordAction` +- [x] Replace `RateLimitSchema` with `RateLimitConfigSchema` from `@objectstack/spec/shared` +- [x] (No action needed) Events module restructuring is internal; imports from `@objectstack/spec/kernel` continue to work --- diff --git a/packages/spec/src/api/discovery.test.ts b/packages/spec/src/api/discovery.test.ts index b87215e80f..606a800dd1 100644 --- a/packages/spec/src/api/discovery.test.ts +++ b/packages/spec/src/api/discovery.test.ts @@ -2,59 +2,12 @@ import { describe, it, expect } from 'vitest'; import { DiscoverySchema, ApiRoutesSchema, - ApiCapabilitiesSchema, ServiceInfoSchema, type DiscoveryResponse, type ApiRoutes, - type ApiCapabilities, type ServiceInfo, } from './discovery.zod'; -describe('ApiCapabilitiesSchema (deprecated — kept for backward compatibility)', () => { - it('should accept valid capabilities', () => { - const capabilities: ApiCapabilities = { - graphql: false, - search: false, - websockets: false, - files: true, - }; - - expect(() => ApiCapabilitiesSchema.parse(capabilities)).not.toThrow(); - }); - - it('should apply default values', () => { - const capabilities = ApiCapabilitiesSchema.parse({}); - - expect(capabilities.graphql).toBe(false); - expect(capabilities.search).toBe(false); - expect(capabilities.websockets).toBe(false); - expect(capabilities.files).toBe(true); - }); - - it('should accept enabled features', () => { - const capabilities = ApiCapabilitiesSchema.parse({ - graphql: true, - search: true, - websockets: true, - files: true, - }); - - expect(capabilities.graphql).toBe(true); - expect(capabilities.search).toBe(true); - }); - - it('should handle minimal capabilities', () => { - const capabilities = ApiCapabilitiesSchema.parse({ - graphql: false, - search: false, - websockets: false, - files: false, - }); - - expect(capabilities.files).toBe(false); - }); -}); - describe('ApiRoutesSchema', () => { it('should accept valid minimal routes', () => { const routes: ApiRoutes = { diff --git a/packages/spec/src/api/discovery.zod.ts b/packages/spec/src/api/discovery.zod.ts index a155470f9e..0591006f57 100644 --- a/packages/spec/src/api/discovery.zod.ts +++ b/packages/spec/src/api/discovery.zod.ts @@ -2,25 +2,6 @@ import { z } from 'zod'; -/** - * API Capabilities Schema - * - * @deprecated Capabilities are now derived from `services` map. - * Each service's `enabled` field replaces the corresponding capability flag. - * Kept for backward compatibility; will be removed in a future major version. - */ -export const ApiCapabilitiesSchema = z.object({ - graphql: z.boolean().default(false), - search: z.boolean().default(false), - websockets: z.boolean().default(false), - files: z.boolean().default(true), - analytics: z.boolean().default(false).describe('Is the Analytics/BI engine enabled?'), - ai: z.boolean().default(false).describe('Is the AI engine enabled?'), - workflow: z.boolean().default(false).describe('Is the Workflow engine enabled?'), - notifications: z.boolean().default(false).describe('Is the Notification service enabled?'), - i18n: z.boolean().default(false).describe('Is the i18n service enabled?'), -}); - /** * Service Status in Discovery Response * Reports per-service availability so clients can adapt their UI accordingly. @@ -135,5 +116,4 @@ export const DiscoverySchema = z.object({ export type DiscoveryResponse = z.infer; export type ApiRoutes = z.infer; -export type ApiCapabilities = z.infer; export type ServiceInfo = z.infer; diff --git a/packages/spec/src/api/endpoint.test.ts b/packages/spec/src/api/endpoint.test.ts index 23cbcfbeab..69091720b8 100644 --- a/packages/spec/src/api/endpoint.test.ts +++ b/packages/spec/src/api/endpoint.test.ts @@ -1,10 +1,10 @@ import { describe, it, expect } from 'vitest'; import { ApiEndpointSchema, - RateLimitSchema, ApiMappingSchema, ApiEndpoint, } from './endpoint.zod'; +import { RateLimitConfigSchema } from '../shared/http.zod'; import { HttpMethod } from './router.zod'; describe('HttpMethod', () => { @@ -23,9 +23,9 @@ describe('HttpMethod', () => { }); }); -describe('RateLimitSchema', () => { +describe('RateLimitConfigSchema', () => { it('should accept valid rate limit', () => { - const rateLimit = RateLimitSchema.parse({}); + const rateLimit = RateLimitConfigSchema.parse({}); expect(rateLimit.enabled).toBe(false); expect(rateLimit.windowMs).toBe(60000); @@ -33,7 +33,7 @@ describe('RateLimitSchema', () => { }); it('should accept custom rate limit', () => { - const rateLimit = RateLimitSchema.parse({ + const rateLimit = RateLimitConfigSchema.parse({ enabled: true, windowMs: 3600000, maxRequests: 1000, @@ -45,7 +45,7 @@ describe('RateLimitSchema', () => { }); it('should accept enabled rate limit', () => { - const rateLimit = RateLimitSchema.parse({ + const rateLimit = RateLimitConfigSchema.parse({ enabled: true, }); diff --git a/packages/spec/src/api/endpoint.zod.ts b/packages/spec/src/api/endpoint.zod.ts index d3cd69f389..430386288a 100644 --- a/packages/spec/src/api/endpoint.zod.ts +++ b/packages/spec/src/api/endpoint.zod.ts @@ -3,12 +3,6 @@ import { z } from 'zod'; import { HttpMethod, RateLimitConfigSchema } from '../shared/http.zod'; -/** - * Rate Limit Strategy - * @deprecated Use RateLimitConfigSchema from shared/http.zod.ts instead - */ -export const RateLimitSchema = RateLimitConfigSchema; - /** * API Mapping Schema * Transform input/output data. @@ -49,7 +43,7 @@ export const ApiEndpointSchema = z.object({ /** Policies */ authRequired: z.boolean().default(true).describe('Require authentication'), - rateLimit: RateLimitSchema.optional().describe('Rate limiting policy'), + rateLimit: RateLimitConfigSchema.optional().describe('Rate limiting policy'), cacheTtl: z.number().optional().describe('Response cache TTL in seconds'), }); diff --git a/packages/spec/src/api/errors.test.ts b/packages/spec/src/api/errors.test.ts index c1237b3a24..ee5ff1cb2a 100644 --- a/packages/spec/src/api/errors.test.ts +++ b/packages/spec/src/api/errors.test.ts @@ -6,8 +6,7 @@ import { FieldErrorSchema, EnhancedApiErrorSchema, ErrorResponseSchema, - getHttpStatusForCategory, - createErrorResponse, + ErrorHttpStatusMap, } from './errors.zod'; describe('ErrorCategory', () => { @@ -210,69 +209,16 @@ describe('ErrorResponseSchema', () => { }); }); -describe('getHttpStatusForCategory', () => { - it('should return correct HTTP status for each category', () => { - expect(getHttpStatusForCategory('validation')).toBe(400); - expect(getHttpStatusForCategory('authentication')).toBe(401); - expect(getHttpStatusForCategory('authorization')).toBe(403); - expect(getHttpStatusForCategory('not_found')).toBe(404); - expect(getHttpStatusForCategory('conflict')).toBe(409); - expect(getHttpStatusForCategory('rate_limit')).toBe(429); - expect(getHttpStatusForCategory('server')).toBe(500); - expect(getHttpStatusForCategory('external')).toBe(502); - expect(getHttpStatusForCategory('maintenance')).toBe(503); - }); -}); - -describe('createErrorResponse', () => { - it('should create basic error response', () => { - const response = createErrorResponse( - 'validation_error', - 'Validation failed' - ); - - expect(response.success).toBe(false); - expect(response.error.code).toBe('validation_error'); - expect(response.error.message).toBe('Validation failed'); - expect(response.error.category).toBe('validation'); - expect(response.error.httpStatus).toBe(400); - }); - - it('should create error response with options', () => { - const response = createErrorResponse( - 'permission_denied', - 'Access denied', - { - retryable: false, - documentation: 'https://docs.example.com', - details: { requiredPermission: 'admin' }, - } - ); - - expect(response.error.category).toBe('authorization'); - expect(response.error.httpStatus).toBe(403); - expect(response.error.retryable).toBe(false); - expect(response.error.documentation).toBe('https://docs.example.com'); - }); - - it('should infer correct category from code', () => { - const validationError = createErrorResponse('invalid_field', 'Invalid field'); - expect(validationError.error.category).toBe('validation'); - - const authError = createErrorResponse('expired_token', 'Token expired'); - expect(authError.error.category).toBe('authentication'); - - const authzError = createErrorResponse('insufficient_privileges', 'Insufficient privileges'); - expect(authzError.error.category).toBe('authorization'); - - const notFoundError = createErrorResponse('record_not_found', 'Record not found'); - expect(notFoundError.error.category).toBe('not_found'); - }); - - it('should include timestamp', () => { - const response = createErrorResponse('internal_error', 'Server error'); - - expect(response.error.timestamp).toBeDefined(); - expect(new Date(response.error.timestamp!).getTime()).toBeGreaterThan(0); +describe('ErrorHttpStatusMap', () => { + it('should map correct HTTP status for each category', () => { + expect(ErrorHttpStatusMap['validation']).toBe(400); + expect(ErrorHttpStatusMap['authentication']).toBe(401); + expect(ErrorHttpStatusMap['authorization']).toBe(403); + expect(ErrorHttpStatusMap['not_found']).toBe(404); + expect(ErrorHttpStatusMap['conflict']).toBe(409); + expect(ErrorHttpStatusMap['rate_limit']).toBe(429); + expect(ErrorHttpStatusMap['server']).toBe(500); + expect(ErrorHttpStatusMap['external']).toBe(502); + expect(ErrorHttpStatusMap['maintenance']).toBe(503); }); }); diff --git a/packages/spec/src/api/errors.zod.ts b/packages/spec/src/api/errors.zod.ts index 8fe45faa27..dd4633380f 100644 --- a/packages/spec/src/api/errors.zod.ts +++ b/packages/spec/src/api/errors.zod.ts @@ -266,71 +266,3 @@ export const ErrorResponseSchema = z.object({ }); export type ErrorResponse = z.infer; - -// ========================================== -// Helper Functions -// ========================================== - -/** - * Get HTTP status code for an error category - * @deprecated Move to @objectstack/core. Will be removed from spec in v3.0.0 - */ -export function getHttpStatusForCategory(category: ErrorCategory): number { - return ErrorHttpStatusMap[category] || 500; -} - -/** - * Create a standardized error response - * @deprecated Move to @objectstack/core. Will be removed from spec in v3.0.0 - */ -export function createErrorResponse( - code: StandardErrorCode, - message: string, - options?: Partial -): ErrorResponse { - const category = getCategoryForCode(code); - - return { - success: false, - error: { - code, - message, - category, - httpStatus: getHttpStatusForCategory(category), - timestamp: new Date().toISOString(), - retryable: false, - ...options, - }, - }; -} - -/** - * Infer error category from error code - */ -function getCategoryForCode(code: StandardErrorCode): ErrorCategory { - if (code.includes('validation') || code.includes('invalid') || code.includes('missing') || code.includes('duplicate')) { - return 'validation'; - } - if (code.includes('unauthenticated') || code.includes('token') || code.includes('credentials') || code.includes('session')) { - return 'authentication'; - } - if (code.includes('permission') || code.includes('privileges') || code.includes('accessible') || code.includes('restricted')) { - return 'authorization'; - } - if (code.includes('not_found')) { - return 'not_found'; - } - if (code.includes('conflict') || code.includes('concurrent') || code.includes('lock')) { - return 'conflict'; - } - if (code.includes('rate_limit') || code.includes('quota')) { - return 'rate_limit'; - } - if (code.includes('external') || code.includes('integration') || code.includes('webhook')) { - return 'external'; - } - if (code.includes('maintenance')) { - return 'maintenance'; - } - return 'server'; -} diff --git a/packages/spec/src/api/realtime-shared.test.ts b/packages/spec/src/api/realtime-shared.test.ts index 2d3300dfd3..3d4b5d0130 100644 --- a/packages/spec/src/api/realtime-shared.test.ts +++ b/packages/spec/src/api/realtime-shared.test.ts @@ -117,7 +117,7 @@ describe('Cross-protocol consistency', () => { const testStatuses = ['online', 'away', 'busy', 'offline']; testStatuses.forEach(status => { - expect(() => realtime.RealtimePresenceStatus.parse(status)).not.toThrow(); + expect(() => realtime.PresenceStatus.parse(status)).not.toThrow(); expect(() => websocket.WebSocketPresenceStatus.parse(status)).not.toThrow(); expect(() => PresenceStatus.parse(status)).not.toThrow(); }); diff --git a/packages/spec/src/api/realtime.test.ts b/packages/spec/src/api/realtime.test.ts index 80e8be84df..fdb13a733b 100644 --- a/packages/spec/src/api/realtime.test.ts +++ b/packages/spec/src/api/realtime.test.ts @@ -4,14 +4,13 @@ import { RealtimeEventType, SubscriptionEventSchema, SubscriptionSchema, - RealtimePresenceStatus, RealtimePresenceSchema, - RealtimeAction, RealtimeEventSchema, type Subscription, type RealtimePresence, type RealtimeEvent, } from './realtime.zod'; +import { PresenceStatus, RealtimeRecordAction } from './realtime-shared.zod'; describe('TransportProtocol', () => { it('should accept valid transport protocols', () => { @@ -190,20 +189,20 @@ describe('SubscriptionSchema', () => { }); }); -describe('RealtimePresenceStatus', () => { +describe('PresenceStatus', () => { it('should accept valid presence statuses', () => { - expect(() => RealtimePresenceStatus.parse('online')).not.toThrow(); - expect(() => RealtimePresenceStatus.parse('away')).not.toThrow(); - expect(() => RealtimePresenceStatus.parse('offline')).not.toThrow(); + expect(() => PresenceStatus.parse('online')).not.toThrow(); + expect(() => PresenceStatus.parse('away')).not.toThrow(); + expect(() => PresenceStatus.parse('offline')).not.toThrow(); }); it('should reject invalid presence statuses', () => { - expect(() => RealtimePresenceStatus.parse('idle')).toThrow(); - expect(() => RealtimePresenceStatus.parse('')).toThrow(); + expect(() => PresenceStatus.parse('idle')).toThrow(); + expect(() => PresenceStatus.parse('')).toThrow(); }); it('should accept busy status', () => { - expect(() => RealtimePresenceStatus.parse('busy')).not.toThrow(); + expect(() => PresenceStatus.parse('busy')).not.toThrow(); }); }); @@ -282,17 +281,17 @@ describe('RealtimePresenceSchema', () => { }); }); -describe('RealtimeAction', () => { +describe('RealtimeRecordAction', () => { it('should accept valid actions', () => { - expect(() => RealtimeAction.parse('created')).not.toThrow(); - expect(() => RealtimeAction.parse('updated')).not.toThrow(); - expect(() => RealtimeAction.parse('deleted')).not.toThrow(); + expect(() => RealtimeRecordAction.parse('created')).not.toThrow(); + expect(() => RealtimeRecordAction.parse('updated')).not.toThrow(); + expect(() => RealtimeRecordAction.parse('deleted')).not.toThrow(); }); it('should reject invalid actions', () => { - expect(() => RealtimeAction.parse('inserted')).toThrow(); - expect(() => RealtimeAction.parse('modified')).toThrow(); - expect(() => RealtimeAction.parse('')).toThrow(); + expect(() => RealtimeRecordAction.parse('inserted')).toThrow(); + expect(() => RealtimeRecordAction.parse('modified')).toThrow(); + expect(() => RealtimeRecordAction.parse('')).toThrow(); }); }); diff --git a/packages/spec/src/api/realtime.zod.ts b/packages/spec/src/api/realtime.zod.ts index 1a5ba80e34..0c3d5c42c7 100644 --- a/packages/spec/src/api/realtime.zod.ts +++ b/packages/spec/src/api/realtime.zod.ts @@ -1,7 +1,7 @@ // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. import { z } from 'zod'; -import { PresenceStatus, RealtimeRecordAction, BasePresenceSchema } from './realtime-shared.zod'; +import { RealtimeRecordAction, BasePresenceSchema } from './realtime-shared.zod'; // Re-export shared types for backward compatibility export { PresenceStatus, RealtimeRecordAction, BasePresenceSchema } from './realtime-shared.zod'; @@ -55,15 +55,6 @@ export const SubscriptionSchema = z.object({ export type Subscription = z.infer; -/** - * Presence Status Enum - * @deprecated Use `PresenceStatus` from `realtime-shared.zod.ts` instead. - * Kept for backward compatibility. - */ -export const RealtimePresenceStatus = PresenceStatus; - -export type RealtimePresenceStatus = z.infer; - /** * Presence Schema * Tracks user online status and metadata. @@ -73,15 +64,6 @@ export const RealtimePresenceSchema = BasePresenceSchema; export type RealtimePresence = z.infer; -/** - * Realtime Action Enum - * @deprecated Use `RealtimeRecordAction` from `realtime-shared.zod.ts` instead. - * Kept for backward compatibility. - */ -export const RealtimeAction = RealtimeRecordAction; - -export type RealtimeAction = z.infer; - /** * Realtime Event Schema * Represents a realtime synchronization event @@ -90,7 +72,7 @@ export const RealtimeEventSchema = z.object({ id: z.string().uuid().describe('Unique event identifier'), type: z.string().describe('Event type (e.g., record.created, record.updated)'), object: z.string().optional().describe('Object name the event relates to'), - action: RealtimeAction.optional().describe('Action performed'), + action: RealtimeRecordAction.optional().describe('Action performed'), payload: z.record(z.string(), z.unknown()).describe('Event payload data'), timestamp: z.string().datetime().describe('ISO 8601 datetime when event occurred'), userId: z.string().optional().describe('User who triggered the event'), diff --git a/packages/spec/src/hub/index.ts b/packages/spec/src/hub/index.ts deleted file mode 100644 index 19fe6d4b4d..0000000000 --- a/packages/spec/src/hub/index.ts +++ /dev/null @@ -1,19 +0,0 @@ -// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. - -/** - * Hub Protocol (Re-exports) - * - * Hub protocols have been reorganized: - * - tenant, license, registry-config → @objectstack/spec/system - * - plugin-registry, plugin-security → @objectstack/spec/kernel - * - * Cloud-specific protocols (marketplace, composer, space, hub-federation) - * have been migrated to the cloud project. - * - * @deprecated Import from '@objectstack/spec/system' or '@objectstack/spec/kernel' directly. - */ -export * from '../system/tenant.zod'; -export * from '../system/license.zod'; -export * from '../system/registry-config.zod'; -export * from '../kernel/plugin-registry.zod'; -export * from '../kernel/plugin-security.zod'; diff --git a/packages/spec/src/index.ts b/packages/spec/src/index.ts index 61c03ab98f..cfbae641d1 100644 --- a/packages/spec/src/index.ts +++ b/packages/spec/src/index.ts @@ -53,7 +53,6 @@ export * as Kernel from './kernel'; export * as Cloud from './cloud'; export * as QA from './qa'; export * as Identity from './identity'; -export * as Hub from './hub'; export * as AI from './ai'; export * as API from './api'; @@ -74,5 +73,5 @@ export { export * from './stack.zod'; -export { definePlugin, type PluginContext } from './kernel/plugin.zod'; +export { type PluginContext } from './kernel/plugin.zod'; diff --git a/packages/spec/src/kernel/events/bus.zod.ts b/packages/spec/src/kernel/events/bus.zod.ts index 4a6303443e..5e5a4fdda8 100644 --- a/packages/spec/src/kernel/events/bus.zod.ts +++ b/packages/spec/src/kernel/events/bus.zod.ts @@ -74,31 +74,3 @@ export const EventBusConfigSchema = z.object({ }); export type EventBusConfig = z.infer; - -// ========================================== -// Helper Functions -// ========================================== - -/** - * Helper to create event bus configuration - * @deprecated Move to `@objectstack/core`. Will be removed from spec in v3.0.0. - */ -export function createEventBusConfig>(config: T): T { - return config; -} - -/** - * Helper to create event type definition - * @deprecated Move to `@objectstack/core`. Will be removed from spec in v3.0.0. - */ -export function createEventTypeDefinition>(definition: T): T { - return definition; -} - -/** - * Helper to create event webhook configuration - * @deprecated Move to `@objectstack/core`. Will be removed from spec in v3.0.0. - */ -export function createEventWebhookConfig>(config: T): T { - return config; -} diff --git a/packages/spec/src/kernel/metadata-plugin.zod.ts b/packages/spec/src/kernel/metadata-plugin.zod.ts index 6a978e8d71..eb5c7aadc6 100644 --- a/packages/spec/src/kernel/metadata-plugin.zod.ts +++ b/packages/spec/src/kernel/metadata-plugin.zod.ts @@ -1,7 +1,7 @@ // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. import { z } from 'zod'; -import { MetadataManagerConfigSchema, MetadataFallbackStrategySchema } from './metadata-loader.zod'; +import { MetadataManagerConfigSchema } from './metadata-loader.zod'; import { MergeStrategyConfigSchema, CustomizationPolicySchema } from './metadata-customization.zod'; /** diff --git a/packages/spec/src/kernel/plugin.zod.ts b/packages/spec/src/kernel/plugin.zod.ts index 730e371aec..bd2421d353 100644 --- a/packages/spec/src/kernel/plugin.zod.ts +++ b/packages/spec/src/kernel/plugin.zod.ts @@ -98,12 +98,3 @@ export const PluginSchema = PluginLifecycleSchema.extend({ }); export type PluginDefinition = z.infer; - -/** - * Define an ObjectStack Plugin - * Helper function for creating type-safe plugin definitions - * @deprecated Move to @objectstack/core. Will be removed from spec in v3.0.0 - */ -export function definePlugin(config: PluginDefinition): PluginDefinition { - return config; -} diff --git a/packages/spec/src/stack.zod.ts b/packages/spec/src/stack.zod.ts index 9db97476af..7922dd86d9 100644 --- a/packages/spec/src/stack.zod.ts +++ b/packages/spec/src/stack.zod.ts @@ -31,7 +31,6 @@ import { SharingRuleSchema } from './security/sharing.zod'; import { PolicySchema } from './security/policy.zod'; import { ApiEndpointSchema } from './api/endpoint.zod'; -import { ApiCapabilitiesSchema } from './api/discovery.zod'; import { FeatureFlagSchema } from './kernel/feature.zod'; // AI Protocol @@ -360,7 +359,17 @@ export const ObjectOSCapabilitiesSchema = z.object({ /** Available APIs */ apis: z.array(ApiEndpointSchema).optional().describe('Available System & Business APIs'), - network: ApiCapabilitiesSchema.optional().describe('Network Capabilities (GraphQL, WS, etc.)'), + network: z.object({ + graphql: z.boolean().default(false), + search: z.boolean().default(false), + websockets: z.boolean().default(false), + files: z.boolean().default(true), + analytics: z.boolean().default(false).describe('Is the Analytics/BI engine enabled?'), + ai: z.boolean().default(false).describe('Is the AI engine enabled?'), + workflow: z.boolean().default(false).describe('Is the Workflow engine enabled?'), + notifications: z.boolean().default(false).describe('Is the Notification service enabled?'), + i18n: z.boolean().default(false).describe('Is the i18n service enabled?'), + }).optional().describe('Network Capabilities (GraphQL, WS, etc.)'), /** Introspection */ systemObjects: z.array(z.string()).optional().describe('List of globally available System Objects'), diff --git a/packages/spec/src/ui/action.zod.ts b/packages/spec/src/ui/action.zod.ts index f373b72ae6..7f394080bb 100644 --- a/packages/spec/src/ui/action.zod.ts +++ b/packages/spec/src/ui/action.zod.ts @@ -66,10 +66,6 @@ export const ActionSchema = z.object({ 'action:group' // Button Group ]).optional().describe('Visual component override'), - /** @deprecated Use `locations` instead. Will be removed in v3.0.0 */ - location: z.unknown().optional() - .describe('DEPRECATED: Use `locations` field instead. Scheduled for removal in v3.0.0'), - /** What type of interaction? */ type: z.enum(['script', 'url', 'modal', 'flow', 'api']).default('script').describe('Action functionality type'), diff --git a/packages/spec/src/ui/i18n.zod.ts b/packages/spec/src/ui/i18n.zod.ts index 7d10f4af5e..fade950e15 100644 --- a/packages/spec/src/ui/i18n.zod.ts +++ b/packages/spec/src/ui/i18n.zod.ts @@ -23,7 +23,7 @@ export const I18nObjectSchema = z.object({ defaultValue: z.string().optional().describe('Fallback value when translation key is not found'), /** Interpolation parameters for dynamic translations */ - params: z.record(z.string(), z.any()).optional().describe('Interpolation parameters (e.g., { count: 5 })'), + params: z.record(z.string(), z.union([z.string(), z.number(), z.boolean()])).optional().describe('Interpolation parameters (e.g., { count: 5 })'), }); export type I18nObject = z.infer;