From 0a5c6c023a997da5b192ef93fa7ae13edaa602c4 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 27 Jul 2026 09:44:15 +0000 Subject: [PATCH] feat(client,spec)!: delete 21 dead SDK methods + the four ghost route tables under them (#3612) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Five client families built URLs no server surface mounts (dispatcher, REST, and autonomous service mounts all checked in the #3587 audit): permissions x3, realtime x6, workflow x3, views x5, notifications device/prefs x4. Every call was a guaranteed 404. Each family was underwritten only by an unconsumed spec DEFAULT_*_ROUTES table — the same disease DEFAULT_DISPATCHER_ROUTES had (#3586) — so DEFAULT_PERMISSION/VIEW/ WORKFLOW/REALTIME_ROUTES go with them; getDefaultRouteRegistrations() returns 9. ApiRouteType loses its client-only 'views'|'permissions' extras. Kept: events (local buffer, no HTTP), notifications list/markRead/ markAllRead (dispatcher-served), approvals.* (ADR-0019), and meta.getLegalNextStates (the real FSM read). Docs/README/integration-test spec swept; api-surface.json regenerated (-4 exports). Re-adding any of these surfaces now requires the server route to exist and a route-ledger row proving it (#3569/#3609 guards). The one external consumer (objectui useClientNotifications dead delegates) is trimmed in a companion objectui change. Closes #3612 Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01LX9ut3MK3KykE11S9bJmv5 --- .changeset/remove-dead-client-surfaces.md | 41 +++ content/docs/api/client-sdk.mdx | 43 +-- .../client/CLIENT_SERVER_INTEGRATION_TESTS.md | 162 +-------- packages/client/README.md | 59 +--- packages/client/src/client.test.ts | 268 -------------- packages/client/src/index.ts | 300 +--------------- packages/spec/api-surface.json | 4 - packages/spec/src/api/plugin-rest-api.test.ts | 82 +---- packages/spec/src/api/plugin-rest-api.zod.ts | 333 +----------------- 9 files changed, 114 insertions(+), 1178 deletions(-) create mode 100644 .changeset/remove-dead-client-surfaces.md diff --git a/.changeset/remove-dead-client-surfaces.md b/.changeset/remove-dead-client-surfaces.md new file mode 100644 index 0000000000..ca081589c4 --- /dev/null +++ b/.changeset/remove-dead-client-surfaces.md @@ -0,0 +1,41 @@ +--- +"@objectstack/client": minor +"@objectstack/spec": minor +--- + +feat(client,spec)!: delete the 21 dead SDK methods and the four ghost route +tables that underwrote them (#3612, #3587 finding) + +Five client surface families built URLs that exist on NO server surface — +not the dispatcher, not `@objectstack/rest`, not the autonomous service +mounts — so every call was a guaranteed 404: + +- `permissions` (check, getObjectPermissions, getEffectivePermissions) +- `realtime` (connect, disconnect, subscribe, unsubscribe, setPresence, + getPresence) — `service-realtime` registers zero HTTP routes and the + dispatcher deliberately never advertises `/realtime` +- `workflow` (getConfig, getState, transition) +- `views` CRUD (list, get, create, update, delete) — no `/ui/views` route + anywhere +- `notifications` device/preference helpers (registerDevice, + unregisterDevice, getPreferences, updatePreferences) — the ADR-0012 + server side was never built + +Each family was underwritten only by an unconsumed spec `DEFAULT_*_ROUTES` +table — the same disease `DEFAULT_DISPATCHER_ROUTES` had (#3586) — so +`DEFAULT_PERMISSION_ROUTES`, `DEFAULT_VIEW_ROUTES`, `DEFAULT_WORKFLOW_ROUTES`, +and `DEFAULT_REALTIME_ROUTES` are deleted with them; +`getDefaultRouteRegistrations()` now returns 9 registrations. +`ApiRouteType` loses its client-only `'views' | 'permissions'` extras. + +Kept: `client.events` (explicitly local in-memory buffer, no HTTP), +`notifications.list/markRead/markAllRead` (dispatcher-served), +`approvals.*` (ADR-0019 — the real approval decision API), and +`meta.getLegalNextStates` (the real FSM read). + +Breaking for anyone calling the removed methods — a repo-wide and +objectui-wide sweep found one consumer (`useClientNotifications`'s dead +device/preference delegates, trimmed in the objectui companion change); +shipped as minor per the launch-window convention (cf. #3562/#3581/#3595). +Re-adding any of these surfaces requires the server route to exist and a +route-ledger row proving it (#3569/#3609 guards). diff --git a/content/docs/api/client-sdk.mdx b/content/docs/api/client-sdk.mdx index 3388d02dff..63b79681d5 100644 --- a/content/docs/api/client-sdk.mdx +++ b/content/docs/api/client-sdk.mdx @@ -112,10 +112,7 @@ The `@objectstack/client` SDK aims to implement the ObjectStack API protocol spe | **meta** | ✅ | 11 | Metadata read/write, published versions & drafts (ADR-0033), FSM introspection (ADR-0020) | | **data** | ✅ | 10 | CRUD & query operations | | **auth** | ✅ | 5 | Authentication & user management | -| **permissions** | ✅ | 3 | Access control checks | | **packages** | ✅ | 17 | Package lifecycle: install/enable, drafts (ADR-0033), commits & rollback (ADR-0067), export/duplicate (ADR-0070) | -| **views** | ✅ | 5 | UI view definitions | -| **workflow** | ✅ | 3 | Workflow state transitions | | **analytics** | ✅ | 3 | Analytics queries | | **automation** | ✅ | 18 | Flow CRUD, trigger/execute, runs, screen-flow resume, descriptor/status registries | | **actions** | ✅ | 2 | Server-registered action handlers (`engine.registerAction`) | @@ -124,10 +121,13 @@ The `@objectstack/client` SDK aims to implement the ObjectStack API protocol spe | **security** | ✅ | 3 | Suggested audience bindings (admin) | | **storage** | ✅ | 2 | File upload & download | | **i18n** | ✅ | 3 | Internationalization | -| **notifications** | 🟡 | 7 | Legacy notification helpers; receipt/inbox cut-over pending | -| **realtime** | ✅ | 6 | Connection/subscription/presence calls (server transport is plugin-provided) | +| **notifications** | ✅ | 3 | List, mark-read, mark-all-read (inbox/receipt spine, ADR-0030) | | **ai** | ✅ | 3 | AI services (NLQ, suggest, insights) | +The former `permissions`, `views`, `workflow`, and `realtime` namespaces (and +the notifications device/preference helpers) were removed in #3612: no server +surface ever mounted their routes, so every call was a guaranteed 404. + **Coverage is CI-enforced, not hand-asserted**: the #3563 route ledger ([`route-ledger.ts`](https://github.com/objectstack-ai/objectstack/blob/main/packages/runtime/src/route-ledger.ts)) @@ -275,16 +275,6 @@ await client.auth.me(); await client.auth.logout(); await client.auth.refreshToken('refresh-token-string'); -// Permissions — Access control checks -await client.permissions.check({ object: 'account', action: 'create' }); -await client.permissions.getObjectPermissions('account'); -await client.permissions.getEffectivePermissions(); - -// Workflow — State machine management -await client.workflow.getConfig('approval'); -await client.workflow.getState('approval', recordId); -await client.workflow.transition({ object: 'approval', recordId, transition: 'submit' }); - // Approvals — request-based decision API (ADR-0019) // Approval is a flow node, not a workflow step: decisions are keyed by request id. await client.approvals.listRequests({ status: 'pending' }); // "my approvals" inbox @@ -293,19 +283,7 @@ await client.approvals.approve(requestId, { comment: 'Looks good' }); await client.approvals.reject(requestId, { comment: 'Incomplete' }); await client.approvals.listActions(requestId); // audit trail -// Realtime — WebSocket subscriptions -await client.realtime.connect({ transport: 'websocket' }); -await client.realtime.subscribe({ channel: 'account', events: ['record.updated'] }); -await client.realtime.unsubscribe('subscription-id'); -await client.realtime.setPresence('account', { userId: 'user-123', status: 'online', lastSeen: new Date().toISOString() }); -await client.realtime.getPresence('account'); -await client.realtime.disconnect(); - -// Notifications — legacy helper surface -await client.notifications.registerDevice({ token: 'device-token', platform: 'ios' }); -await client.notifications.unregisterDevice('device-id'); -await client.notifications.getPreferences(); -await client.notifications.updatePreferences({ email: true, push: false }); +// Notifications — inbox/receipt spine (ADR-0030) await client.notifications.list({ read: false }); await client.notifications.markRead(['notif-1', 'notif-2']); await client.notifications.markAllRead(); @@ -374,17 +352,10 @@ await client.security.suggestedBindings.confirm(suggestions[0].id); // Storage — File upload and management await client.storage.upload(fileData, 'user'); await client.storage.getDownloadUrl('file-123'); - -// Views — UI view management -await client.views.list('account'); -await client.views.get('account', viewId); -await client.views.create('account', { name: 'my_view', ... }); -await client.views.update('account', viewId, { ... }); -await client.views.delete('account', viewId); ``` -**Service availability**: Optional services (workflow, ai, etc.) are only available when the corresponding plugin is installed on the server. Always check the `services` map on the discovery result returned by `client.connect()` (cache it yourself — the client has no `discovery` getter) to verify service availability before calling these methods. +**Service availability**: Optional services (automation, ai, etc.) are only available when the corresponding plugin is installed on the server. Always check the `services` map on the discovery result returned by `client.connect()` (cache it yourself — the client has no `discovery` getter) to verify service availability before calling these methods. --- diff --git a/packages/client/CLIENT_SERVER_INTEGRATION_TESTS.md b/packages/client/CLIENT_SERVER_INTEGRATION_TESTS.md index c72e6a59f7..4b66de012e 100644 --- a/packages/client/CLIENT_SERVER_INTEGRATION_TESTS.md +++ b/packages/client/CLIENT_SERVER_INTEGRATION_TESTS.md @@ -14,7 +14,7 @@ This document defines comprehensive integration tests for validating `@objectsta - ObjectStack server instance running - Test database (SQLite/Postgres) with sample data - All core services enabled (metadata, data, auth) - - Optional services enabled (workflow, ai, realtime, etc.) + - Optional services enabled (automation, ai, etc.) 2. **Client Configuration:** ```typescript @@ -43,17 +43,13 @@ packages/client/tests/integration/ ├── 04-data-crud.test.ts # Basic CRUD operations ├── 05-data-batch.test.ts # Batch operations ├── 06-data-query.test.ts # Advanced queries -├── 07-permissions.test.ts # Permission checking -├── 08-workflow.test.ts # Workflow operations -├── 09-realtime.test.ts # Realtime subscriptions -├── 10-notifications.test.ts # Notifications -├── 11-ai.test.ts # AI services -├── 12-i18n.test.ts # Internationalization -├── 13-analytics.test.ts # Analytics queries -├── 14-packages.test.ts # Package management -├── 15-views.test.ts # View management -├── 16-storage.test.ts # File storage -├── 17-automation.test.ts # Automation triggers +├── 07-notifications.test.ts # Notifications +├── 08-ai.test.ts # AI services +├── 09-i18n.test.ts # Internationalization +├── 10-analytics.test.ts # Analytics queries +├── 11-packages.test.ts # Package management +├── 12-storage.test.ts # File storage +├── 13-automation.test.ts # Automation triggers └── helpers/ ├── test-server.ts # Mock/stub server helpers ├── test-data.ts # Test data generators @@ -608,145 +604,15 @@ test('should execute aggregation query', async () => { --- -### 7. Permissions (`07-permissions.test.ts`) +### 7-8. Permissions & Workflow — removed (#3612) -#### TC-PERM-001: Check Create Permission -```typescript -test('should check if user can create records', async () => { - const client = await createAuthenticatedClient(); - - const result = await client.permissions.check({ - object: 'test_contact', - action: 'create' - }); - - expect(result.allowed).toBe(true); - expect(result.deniedFields).toBeUndefined(); -}); -``` - -#### TC-PERM-002: Get Object Permissions -```typescript -test('should retrieve object-level permissions', async () => { - const client = await createAuthenticatedClient(); - - const perms = await client.permissions.getObjectPermissions('test_contact'); - - expect(perms.object).toBe('test_contact'); - expect(perms.permissions).toBeDefined(); - expect(perms.fieldPermissions).toBeDefined(); -}); -``` - -#### TC-PERM-003: Get Effective Permissions -```typescript -test('should get effective permissions for current user', async () => { - const client = await createAuthenticatedClient(); - - const effective = await client.permissions.getEffectivePermissions('test_contact'); - - expect(effective.canCreate).toBeDefined(); - expect(effective.canRead).toBeDefined(); - expect(effective.canEdit).toBeDefined(); - expect(effective.canDelete).toBeDefined(); - expect(effective.fields).toBeDefined(); -}); -``` - ---- - -### 8. Workflow (`08-workflow.test.ts`) - -#### TC-WF-001: Get Workflow Configuration -```typescript -test('should retrieve workflow rules for object', async () => { - const client = await createAuthenticatedClient(); - - const config = await client.workflow.getConfig('test_approval'); - - expect(config.object).toBe('test_approval'); - expect(config.states).toBeDefined(); - expect(config.transitions).toBeDefined(); -}); -``` - -#### TC-WF-002: Get Workflow State -```typescript -test('should get current workflow state and available transitions', async () => { - const client = await createAuthenticatedClient(); - - const record = await client.data.create('test_approval', { - title: 'Test Approval', - status: 'draft' - }); - - const state = await client.workflow.getState('test_approval', record.id); - - expect(state.currentState).toBe('draft'); - expect(state.availableTransitions).toContain('submit'); -}); -``` - -#### TC-WF-003: Execute Workflow Transition -```typescript -test('should execute workflow state transition', async () => { - const client = await createAuthenticatedClient(); - - const record = await client.data.create('test_approval', { - title: 'Test', - status: 'draft' - }); - - const result = await client.workflow.transition({ - object: 'test_approval', - recordId: record.id, - transition: 'submit', - comment: 'Submitting for approval' - }); - - expect(result.success).toBe(true); - expect(result.newState).toBe('pending'); -}); -``` - -#### TC-WF-004: Approve Workflow -```typescript -test('should approve workflow transition', async () => { - const client = await createAuthenticatedClient(); - - const result = await client.workflow.approve({ - object: 'test_approval', - recordId: testRecordId, - comment: 'Approved by manager' - }); - - expect(result.success).toBe(true); - expect(result.newState).toBe('approved'); -}); -``` - -#### TC-WF-005: Reject Workflow -```typescript -test('should reject workflow transition', async () => { - const client = await createAuthenticatedClient(); - - const result = await client.workflow.reject({ - object: 'test_approval', - recordId: testRecordId, - reason: 'Insufficient documentation', - comment: 'Please provide more details' - }); - - expect(result.success).toBe(true); - expect(result.newState).toBe('rejected'); -}); -``` - ---- +The `permissions` and `workflow` client namespaces were deleted: no server +surface ever mounted their routes. State-machine reads live on +`meta.getLegalNextStates`; approval decisions are `client.approvals` (ADR-0019). -### 9-17. Additional Test Categories +### 9-13. Additional Test Categories -*(Similar detailed test cases for remaining namespaces: Realtime, Notifications, AI, i18n, Analytics, Packages, Views, Storage, Automation)* +*(Similar detailed test cases for remaining namespaces: Notifications, AI, i18n, Analytics, Packages, Storage, Automation)** --- diff --git a/packages/client/README.md b/packages/client/README.md index c717cf7b78..76cd9c7946 100644 --- a/packages/client/README.md +++ b/packages/client/README.md @@ -92,25 +92,6 @@ async function main() { console.log('Using cached metadata'); } - // 8. View Storage (New!) - const view = await client.views.create('todo_task', { - name: 'active_tasks', - label: 'Active Tasks', - type: 'list', - visibility: 'public', - query: { - object: 'todo_task', - where: { status: 'active' }, - orderBy: [{ field: 'priority', order: 'desc' }], - limit: 50 - }, - layout: { - columns: [ - { field: 'subject', label: 'Task', width: 200 }, - { field: 'priority', label: 'Priority', width: 100 } - ] - } - }); } ``` @@ -137,13 +118,6 @@ Initializes the client by fetching the system discovery manifest from `/api/v1`. - `delete(object, id)`: Delete record. - `deleteMany(object, ids[], options?)`: Batch delete records (convenience method). -### `client.views` -- `create(object, data)`: Create a new saved view. -- `get(object, viewId)`: Get a saved view by ID. -- `list(object, type?)`: List saved views. -- `update(object, viewId, data)`: Update an existing view. -- `delete(object, viewId)`: Delete a saved view. - ### Query Options The `find` method accepts an options object with canonical field names: - `where`: Filter conditions (WHERE clause). Accepts object or FilterCondition AST. @@ -224,15 +198,18 @@ const data = await retryableRequest(() => ## Protocol Compliance -The SDK ships 20 top-level surfaces: `meta`, `data`, `auth`, `oauth`, -`organizations`, `projects`, `packages`, `views`, `permissions`, `workflow`, +The SDK ships these top-level surfaces: `meta`, `data`, `auth`, `oauth`, +`organizations`, `projects`, `packages`, `keys`, `security`, `shareLinks`, `approvals`, `analytics`, `automation`, `actions`, `storage`, `i18n`, -`notifications`, `realtime`, `ai`, and `events`. +`notifications`, `ai`, and `events`. (The former `views`, `permissions`, +`workflow`, and `realtime` namespaces were removed in #3612 — no server +surface ever mounted their routes.) Coverage against the server's actual route surface is no longer asserted by a -hand-written table — it is audited and CI-enforced by the #3563 route ledger -(`packages/runtime/src/route-ledger.ts` + its conformance tests, one half in -runtime, one half in this package). See +hand-written table — it is audited and CI-enforced by the route ledgers +(`packages/runtime/src/route-ledger.ts` for the dispatcher, #3563; +`packages/rest/src/rest-route-ledger.ts` for the REST surface, #3587 — each +with conformance tests, one half server-side, one half in this package). See `docs/audits/2026-07-dispatcher-client-route-coverage.md` for the audit. ## Available Namespaces @@ -271,27 +248,11 @@ await client.packages.install({ }); await client.packages.enable('plugin-id'); -// Permissions -await client.permissions.check({ object: 'contact', action: 'create' }); -await client.permissions.getObjectPermissions('contact'); -await client.permissions.getEffectivePermissions(); - -// Workflow -await client.workflow.getConfig('approval'); -await client.workflow.getState('approval', recordId); -await client.workflow.transition({ object: 'approval', recordId, transition: 'submit' }); - -// Approvals (approve/reject live here, not on workflow) +// Approvals (approval is a flow node — decisions are keyed by request id) await client.approvals.approve(requestId, 'LGTM'); await client.approvals.reject(requestId, 'Incomplete'); -// Realtime -await client.realtime.connect({ protocol: 'websocket' }); -await client.realtime.subscribe({ channel: 'contact', event: 'update' }); -await client.realtime.setPresence({ status: 'online' }); - // Notifications -await client.notifications.registerDevice({ token: 'device-token', platform: 'ios' }); await client.notifications.list({ unreadOnly: true }); await client.notifications.markRead(['notif-1', 'notif-2']); diff --git a/packages/client/src/client.test.ts b/packages/client/src/client.test.ts index 6f69a05d4c..01f3ec67a1 100644 --- a/packages/client/src/client.test.ts +++ b/packages/client/src/client.test.ts @@ -107,175 +107,6 @@ describe('ObjectStackClient', () => { }); }); -describe('Permissions namespace', () => { - it('should check permission with all params', async () => { - const { client, fetchMock } = createMockClient({ - success: true, - data: { allowed: true, reason: 'owner' } - }); - const result = await client.permissions.check({ - object: 'customer', - action: 'read', - recordId: '123', - field: 'email' - }); - expect(result).toEqual({ allowed: true, reason: 'owner' }); - const url = fetchMock.mock.calls[0][0] as string; - expect(url).toContain('/api/v1/permissions/check'); - expect(url).toContain('object=customer'); - expect(url).toContain('action=read'); - expect(url).toContain('recordId=123'); - expect(url).toContain('field=email'); - }); - - it('should check permission without optional params', async () => { - const { client, fetchMock } = createMockClient({ - success: true, - data: { allowed: false } - }); - const result = await client.permissions.check({ - object: 'order', - action: 'delete' - }); - expect(result).toEqual({ allowed: false }); - const url = fetchMock.mock.calls[0][0] as string; - expect(url).not.toContain('recordId'); - expect(url).not.toContain('field='); - }); - - it('should get object permissions', async () => { - const { client, fetchMock } = createMockClient({ - success: true, - data: { object: 'customer', permissions: { read: true, create: true } } - }); - const result = await client.permissions.getObjectPermissions('customer'); - expect(result.object).toBe('customer'); - const url = fetchMock.mock.calls[0][0] as string; - expect(url).toContain('/api/v1/permissions/objects/customer'); - }); - - it('should get effective permissions', async () => { - const { client, fetchMock } = createMockClient({ - success: true, - data: { roles: ['admin'], permissions: [] } - }); - const result = await client.permissions.getEffectivePermissions(); - expect(result.roles).toEqual(['admin']); - const url = fetchMock.mock.calls[0][0] as string; - expect(url).toContain('/api/v1/permissions/effective'); - }); -}); - -describe('Realtime namespace', () => { - it('should connect to realtime', async () => { - const { client, fetchMock } = createMockClient({ - success: true, - data: { connectionId: 'conn-1', transport: 'websocket' } - }); - const result = await client.realtime.connect({ transport: 'websocket' as any }); - expect(result.connectionId).toBe('conn-1'); - const [url, opts] = fetchMock.mock.calls[0]; - expect(url).toContain('/api/v1/realtime/connect'); - expect(opts.method).toBe('POST'); - }); - - it('should disconnect from realtime', async () => { - const { client, fetchMock } = createMockClient({ success: true }); - await client.realtime.disconnect(); - const [url, opts] = fetchMock.mock.calls[0]; - expect(url).toContain('/api/v1/realtime/disconnect'); - expect(opts.method).toBe('POST'); - }); - - it('should subscribe to a channel', async () => { - const { client, fetchMock } = createMockClient({ - success: true, - data: { subscriptionId: 'sub-1' } - }); - const result = await client.realtime.subscribe({ - channel: 'customer.changes', - events: ['create', 'update'] - }); - expect(result.subscriptionId).toBe('sub-1'); - const body = JSON.parse(fetchMock.mock.calls[0][1].body); - expect(body.channel).toBe('customer.changes'); - expect(body.events).toEqual(['create', 'update']); - }); - - it('should unsubscribe from a channel', async () => { - const { client, fetchMock } = createMockClient({ success: true }); - await client.realtime.unsubscribe('sub-1'); - const body = JSON.parse(fetchMock.mock.calls[0][1].body); - expect(body.subscriptionId).toBe('sub-1'); - }); - - it('should set presence', async () => { - const { client, fetchMock } = createMockClient({ success: true }); - await client.realtime.setPresence('room-1', { status: 'online' } as any); - const [url, opts] = fetchMock.mock.calls[0]; - expect(url).toContain('/api/v1/realtime/presence'); - expect(opts.method).toBe('PUT'); - const body = JSON.parse(opts.body); - expect(body.channel).toBe('room-1'); - expect(body.state.status).toBe('online'); - }); - - it('should get presence for a channel', async () => { - const { client, fetchMock } = createMockClient({ - success: true, - data: { channel: 'room-1', members: [] } - }); - const result = await client.realtime.getPresence('room-1'); - expect(result.channel).toBe('room-1'); - const url = fetchMock.mock.calls[0][0] as string; - expect(url).toContain('/api/v1/realtime/presence/room-1'); - }); -}); - -describe('Workflow namespace', () => { - it('should get workflow config', async () => { - const { client, fetchMock } = createMockClient({ - success: true, - data: { object: 'order', states: ['draft', 'submitted'] } - }); - const result = await client.workflow.getConfig('order'); - expect(result.object).toBe('order'); - const url = fetchMock.mock.calls[0][0] as string; - expect(url).toContain('/api/v1/workflow/order/config'); - }); - - it('should get workflow state', async () => { - const { client, fetchMock } = createMockClient({ - success: true, - data: { state: 'draft', transitions: ['submit'] } - }); - const result = await client.workflow.getState('order', 'rec-1'); - expect(result.state).toBe('draft'); - const url = fetchMock.mock.calls[0][0] as string; - expect(url).toContain('/api/v1/workflow/order/rec-1/state'); - }); - - it('should execute workflow transition', async () => { - const { client, fetchMock } = createMockClient({ - success: true, - data: { success: true, newState: 'submitted' } - }); - const result = await client.workflow.transition({ - object: 'order', - recordId: 'rec-1', - transition: 'submit', - comment: 'Ready for review' - }); - expect(result.newState).toBe('submitted'); - const body = JSON.parse(fetchMock.mock.calls[0][1].body); - expect(body.transition).toBe('submit'); - expect(body.comment).toBe('Ready for review'); - }); - - // ADR-0019: approve/reject left the workflow namespace — they now live on - // `client.approvals` (approval is a flow node, not a workflow step). -}); - describe('Approvals namespace (ADR-0019)', () => { it('should list approval requests with filters', async () => { const { client, fetchMock } = createMockClient({ @@ -345,78 +176,6 @@ describe('Approvals namespace (ADR-0019)', () => { }); }); -describe('Views namespace', () => { - it('should list views for an object', async () => { - const { client, fetchMock } = createMockClient({ - success: true, - data: { views: [{ id: 'v1', name: 'Default' }] } - }); - const result = await client.views.list('customer', 'list'); - expect(result.views).toHaveLength(1); - const url = fetchMock.mock.calls[0][0] as string; - expect(url).toContain('/api/v1/ui/views/customer'); - expect(url).toContain('type=list'); - }); - - it('should list views without type filter', async () => { - const { client, fetchMock } = createMockClient({ - success: true, - data: { views: [] } - }); - await client.views.list('order'); - const url = fetchMock.mock.calls[0][0] as string; - expect(url).toContain('/api/v1/ui/views/order'); - expect(url).not.toContain('type='); - }); - - it('should get a specific view', async () => { - const { client, fetchMock } = createMockClient({ - success: true, - data: { id: 'v1', name: 'Default', type: 'list' } - }); - const result = await client.views.get('customer', 'v1'); - expect(result.id).toBe('v1'); - const url = fetchMock.mock.calls[0][0] as string; - expect(url).toContain('/api/v1/ui/views/customer/v1'); - }); - - it('should create a view', async () => { - const { client, fetchMock } = createMockClient({ - success: true, - data: { id: 'v2', name: 'Custom View' } - }); - const result = await client.views.create('customer', { name: 'Custom View' } as any); - expect(result.id).toBe('v2'); - const [url, opts] = fetchMock.mock.calls[0]; - expect(url).toContain('/api/v1/ui/views/customer'); - expect(opts.method).toBe('POST'); - }); - - it('should update a view', async () => { - const { client, fetchMock } = createMockClient({ - success: true, - data: { id: 'v1', name: 'Updated View' } - }); - const result = await client.views.update('customer', 'v1', { name: 'Updated View' } as any); - expect(result.name).toBe('Updated View'); - const [url, opts] = fetchMock.mock.calls[0]; - expect(url).toContain('/api/v1/ui/views/customer/v1'); - expect(opts.method).toBe('PUT'); - }); - - it('should delete a view', async () => { - const { client, fetchMock } = createMockClient({ - success: true, - data: { deleted: true } - }); - const result = await client.views.delete('customer', 'v1'); - expect(result.deleted).toBe(true); - const [url, opts] = fetchMock.mock.calls[0]; - expect(url).toContain('/api/v1/ui/views/customer/v1'); - expect(opts.method).toBe('DELETE'); - }); -}); - describe('Auth enhancements', () => { it('should register a new user', async () => { const { client, fetchMock } = createMockClient({ @@ -483,33 +242,6 @@ describe('Auth enhancements', () => { }); describe('Notifications namespace', () => { - it('should register a device', async () => { - const { client, fetchMock } = createMockClient({ - success: true, - data: { deviceId: 'dev-1', registered: true } - }); - const result = await client.notifications.registerDevice({ - token: 'push-token', - platform: 'web', - deviceId: 'dev-1' - }); - expect(result.deviceId).toBe('dev-1'); - const [url, opts] = fetchMock.mock.calls[0]; - expect(url).toContain('/api/v1/notifications/devices'); - expect(opts.method).toBe('POST'); - }); - - it('should unregister a device', async () => { - const { client, fetchMock } = createMockClient({ - success: true, - data: { success: true } - }); - await client.notifications.unregisterDevice('dev-1'); - const [url, opts] = fetchMock.mock.calls[0]; - expect(url).toContain('/api/v1/notifications/devices/dev-1'); - expect(opts.method).toBe('DELETE'); - }); - it('should list notifications with filters', async () => { const { client, fetchMock } = createMockClient({ success: true, diff --git a/packages/client/src/index.ts b/packages/client/src/index.ts index 7c76259959..df25b4e534 100644 --- a/packages/client/src/index.ts +++ b/packages/client/src/index.ts @@ -26,33 +26,6 @@ import { CompleteChunkedUploadRequest, CompleteChunkedUploadResponse, UploadProgress, - CheckPermissionRequest, - CheckPermissionResponse, - GetObjectPermissionsResponse, - GetEffectivePermissionsResponse, - RealtimeConnectRequest, - RealtimeConnectResponse, - RealtimeSubscribeRequest, - RealtimeSubscribeResponse, - SetPresenceRequest, - GetPresenceResponse, - GetWorkflowConfigResponse, - GetWorkflowStateResponse, - WorkflowTransitionRequest, - WorkflowTransitionResponse, - ListViewsResponse, - GetViewResponse, - CreateViewRequest, - CreateViewResponse, - UpdateViewRequest, - UpdateViewResponse, - DeleteViewResponse, - RegisterDeviceRequest, - RegisterDeviceResponse, - UnregisterDeviceResponse, - GetNotificationPreferencesResponse, - UpdateNotificationPreferencesRequest, - UpdateNotificationPreferencesResponse, ListNotificationsResponse, MarkNotificationsReadResponse, MarkAllNotificationsReadResponse, @@ -93,10 +66,11 @@ import { RealtimeAPI } from './realtime-api'; /** * Route types that the client can resolve. - * Covers all keys from `ApiRoutes` (the discovery schema) plus - * client-specific virtual routes (`views`, `permissions`). + * Covers all keys from `ApiRoutes` (the discovery schema). The former + * client-only virtual routes (`views`, `permissions`) were removed in #3612 — + * no server surface ever mounted them. */ -export type ApiRouteType = keyof ApiRoutes | 'views' | 'permissions'; +export type ApiRouteType = keyof ApiRoutes; export interface ClientConfig { baseUrl: string; @@ -2771,151 +2745,13 @@ export class ObjectStackClient { return this.realtimeAPI; } - /** - * Permissions Services - */ - permissions = { - /** - * Check if current user has permission for an action on an object - */ - check: async (request: CheckPermissionRequest): Promise => { - const route = this.getRoute('permissions'); - const params = new URLSearchParams({ object: request.object, action: request.action }); - if (request.recordId !== undefined) params.set('recordId', request.recordId); - if (request.field !== undefined) params.set('field', request.field); - const res = await this.fetch(`${this.baseUrl}${route}/check?${params.toString()}`); - return this.unwrapResponse(res); - }, - - /** - * Get all permissions for a specific object - */ - getObjectPermissions: async (object: string): Promise => { - const route = this.getRoute('permissions'); - const res = await this.fetch(`${this.baseUrl}${route}/objects/${encodeURIComponent(object)}`); - return this.unwrapResponse(res); - }, - - /** - * Get effective permissions for the current user - */ - getEffectivePermissions: async (): Promise => { - const route = this.getRoute('permissions'); - const res = await this.fetch(`${this.baseUrl}${route}/effective`); - return this.unwrapResponse(res); - } - }; - - /** - * Realtime Services - */ - realtime = { - /** - * Establish a realtime connection - */ - connect: async (request?: RealtimeConnectRequest): Promise => { - const route = this.getRoute('realtime'); - const res = await this.fetch(`${this.baseUrl}${route}/connect`, { - method: 'POST', - body: JSON.stringify(request || {}) - }); - return this.unwrapResponse(res); - }, - - /** - * Disconnect from realtime services - */ - disconnect: async (): Promise => { - const route = this.getRoute('realtime'); - await this.fetch(`${this.baseUrl}${route}/disconnect`, { - method: 'POST' - }); - }, - - /** - * Subscribe to a channel - */ - subscribe: async (request: RealtimeSubscribeRequest): Promise => { - const route = this.getRoute('realtime'); - const res = await this.fetch(`${this.baseUrl}${route}/subscribe`, { - method: 'POST', - body: JSON.stringify(request) - }); - return this.unwrapResponse(res); - }, - - /** - * Unsubscribe from a channel - */ - unsubscribe: async (subscriptionId: string): Promise => { - const route = this.getRoute('realtime'); - await this.fetch(`${this.baseUrl}${route}/unsubscribe`, { - method: 'POST', - body: JSON.stringify({ subscriptionId }) - }); - }, - - /** - * Set presence state on a channel - */ - setPresence: async (channel: string, state: SetPresenceRequest['state']): Promise => { - const route = this.getRoute('realtime'); - await this.fetch(`${this.baseUrl}${route}/presence`, { - method: 'PUT', - body: JSON.stringify({ channel, state }) - }); - }, - - /** - * Get presence information for a channel - */ - getPresence: async (channel: string): Promise => { - const route = this.getRoute('realtime'); - const res = await this.fetch(`${this.baseUrl}${route}/presence/${encodeURIComponent(channel)}`); - return this.unwrapResponse(res); - } - }; - - /** - * Workflow Services - */ - workflow = { - /** - * Get workflow configuration for an object - */ - getConfig: async (object: string): Promise => { - const route = this.getRoute('workflow'); - const res = await this.fetch(`${this.baseUrl}${route}/${encodeURIComponent(object)}/config`); - return this.unwrapResponse(res); - }, - - /** - * Get current workflow state for a record - */ - getState: async (object: string, recordId: string): Promise => { - const route = this.getRoute('workflow'); - const res = await this.fetch(`${this.baseUrl}${route}/${encodeURIComponent(object)}/${encodeURIComponent(recordId)}/state`); - return this.unwrapResponse(res); - }, - - /** - * Execute a workflow state transition - */ - transition: async (request: WorkflowTransitionRequest): Promise => { - const route = this.getRoute('workflow'); - const res = await this.fetch(`${this.baseUrl}${route}/${encodeURIComponent(request.object)}/${encodeURIComponent(request.recordId)}/transition`, { - method: 'POST', - body: JSON.stringify({ - transition: request.transition, - comment: request.comment, - data: request.data - }) - }); - return this.unwrapResponse(res); - } - // ADR-0019: approve/reject are no longer workflow operations. Approval is a - // flow node — see the `approvals` namespace below for recording decisions. - }; + // The former `permissions`, `realtime`, and `workflow` namespaces were + // removed in #3612: every method targeted a route no server surface mounts + // (each family was underwritten only by an unconsumed spec DEFAULT_*_ROUTES + // table), so every call was a guaranteed 404. Server-backed state-machine + // reads live under `meta.getLegalNextStates`; approval decisions are the + // `approvals` namespace (ADR-0019); realtime events are the local `events` + // buffer until a real HTTP/WS session protocol exists. /** * Approval Services (ADR-0019) @@ -3020,115 +2856,19 @@ export class ObjectStackClient { } }; - /** - * Views CRUD Services - */ - views = { - /** - * List views for an object - */ - list: async (object: string, type?: 'list' | 'form'): Promise => { - const route = this.getRoute('views'); - const params = new URLSearchParams(); - if (type) params.set('type', type); - const qs = params.toString(); - const res = await this.fetch(`${this.baseUrl}${route}/${encodeURIComponent(object)}${qs ? `?${qs}` : ''}`); - return this.unwrapResponse(res); - }, - - /** - * Get a specific view - */ - get: async (object: string, viewId: string): Promise => { - const route = this.getRoute('views'); - const res = await this.fetch(`${this.baseUrl}${route}/${encodeURIComponent(object)}/${encodeURIComponent(viewId)}`); - return this.unwrapResponse(res); - }, - - /** - * Create a new view - */ - create: async (object: string, data: CreateViewRequest['data']): Promise => { - const route = this.getRoute('views'); - const res = await this.fetch(`${this.baseUrl}${route}/${encodeURIComponent(object)}`, { - method: 'POST', - body: JSON.stringify({ object, data }) - }); - return this.unwrapResponse(res); - }, - - /** - * Update an existing view - */ - update: async (object: string, viewId: string, data: UpdateViewRequest['data']): Promise => { - const route = this.getRoute('views'); - const res = await this.fetch(`${this.baseUrl}${route}/${encodeURIComponent(object)}/${encodeURIComponent(viewId)}`, { - method: 'PUT', - body: JSON.stringify({ object, viewId, data }) - }); - return this.unwrapResponse(res); - }, - - /** - * Delete a view - */ - delete: async (object: string, viewId: string): Promise => { - const route = this.getRoute('views'); - const res = await this.fetch(`${this.baseUrl}${route}/${encodeURIComponent(object)}/${encodeURIComponent(viewId)}`, { - method: 'DELETE' - }); - return this.unwrapResponse(res); - } - }; + // The former `views` CRUD namespace was removed in #3612 — no server + // surface mounts /ui/views (both surfaces serve only /ui/view/:object…). + // View definitions are metadata: read and save them via `meta.*`. /** * Notification Services + * + * Device registration and preference methods were removed in #3612 — the + * /notifications/devices and /notifications/preferences server routes that + * ADR-0012 describes were never built. They return together with the server + * (and a route-ledger row keeps them honest). */ notifications = { - /** - * Register a device for push notifications - */ - registerDevice: async (request: RegisterDeviceRequest): Promise => { - const route = this.getRoute('notifications'); - const res = await this.fetch(`${this.baseUrl}${route}/devices`, { - method: 'POST', - body: JSON.stringify(request) - }); - return this.unwrapResponse(res); - }, - - /** - * Unregister a device from push notifications - */ - unregisterDevice: async (deviceId: string): Promise => { - const route = this.getRoute('notifications'); - const res = await this.fetch(`${this.baseUrl}${route}/devices/${encodeURIComponent(deviceId)}`, { - method: 'DELETE' - }); - return this.unwrapResponse(res); - }, - - /** - * Get notification preferences for the current user - */ - getPreferences: async (): Promise => { - const route = this.getRoute('notifications'); - const res = await this.fetch(`${this.baseUrl}${route}/preferences`); - return this.unwrapResponse(res); - }, - - /** - * Update notification preferences - */ - updatePreferences: async (preferences: UpdateNotificationPreferencesRequest['preferences']): Promise => { - const route = this.getRoute('notifications'); - const res = await this.fetch(`${this.baseUrl}${route}/preferences`, { - method: 'PUT', - body: JSON.stringify({ preferences }) - }); - return this.unwrapResponse(res); - }, - /** * List notifications for the current user */ @@ -3731,11 +3471,9 @@ export class ObjectStackClient { storage: '/api/v1/storage', automation: '/api/v1/automation', packages: '/api/v1/packages', - permissions: '/api/v1/permissions', realtime: '/api/v1/realtime', workflow: '/api/v1/workflow', approvals: '/api/v1/approvals', - views: '/api/v1/ui/views', notifications: '/api/v1/notifications', ai: '/api/v1/ai', i18n: '/api/v1/i18n', diff --git a/packages/spec/api-surface.json b/packages/spec/api-surface.json index 2b7faf8a2c..7213663cfa 100644 --- a/packages/spec/api-surface.json +++ b/packages/spec/api-surface.json @@ -2370,11 +2370,7 @@ "DEFAULT_I18N_ROUTES (const)", "DEFAULT_METADATA_ROUTES (const)", "DEFAULT_NOTIFICATION_ROUTES (const)", - "DEFAULT_PERMISSION_ROUTES (const)", - "DEFAULT_REALTIME_ROUTES (const)", "DEFAULT_VERSIONING_CONFIG (const)", - "DEFAULT_VIEW_ROUTES (const)", - "DEFAULT_WORKFLOW_ROUTES (const)", "DataEvent (type)", "DataEventSchema (const)", "DataEventType (type)", diff --git a/packages/spec/src/api/plugin-rest-api.test.ts b/packages/spec/src/api/plugin-rest-api.test.ts index a4b6d44618..7f72f0fdac 100644 --- a/packages/spec/src/api/plugin-rest-api.test.ts +++ b/packages/spec/src/api/plugin-rest-api.test.ts @@ -13,10 +13,6 @@ import { DEFAULT_METADATA_ROUTES, DEFAULT_DATA_CRUD_ROUTES, DEFAULT_BATCH_ROUTES, - DEFAULT_PERMISSION_ROUTES, - DEFAULT_VIEW_ROUTES, - DEFAULT_WORKFLOW_ROUTES, - DEFAULT_REALTIME_ROUTES, DEFAULT_NOTIFICATION_ROUTES, DEFAULT_AI_ROUTES, DEFAULT_I18N_ROUTES, @@ -508,54 +504,6 @@ describe('plugin-rest-api.zod', () => { }); }); - it('should validate DEFAULT_PERMISSION_ROUTES', () => { - expect(DEFAULT_PERMISSION_ROUTES.prefix).toBe('/api/v1/auth'); - expect(DEFAULT_PERMISSION_ROUTES.service).toBe('auth'); - expect(DEFAULT_PERMISSION_ROUTES.category).toBe('permission'); - expect(DEFAULT_PERMISSION_ROUTES.methods).toContain('checkPermission'); - expect(DEFAULT_PERMISSION_ROUTES.methods).toContain('getObjectPermissions'); - expect(DEFAULT_PERMISSION_ROUTES.methods).toContain('getEffectivePermissions'); - expect(DEFAULT_PERMISSION_ROUTES.endpoints).toHaveLength(3); - }); - - it('should validate DEFAULT_VIEW_ROUTES', () => { - expect(DEFAULT_VIEW_ROUTES.prefix).toBe('/api/v1/ui'); - expect(DEFAULT_VIEW_ROUTES.service).toBe('ui'); - expect(DEFAULT_VIEW_ROUTES.category).toBe('ui'); - expect(DEFAULT_VIEW_ROUTES.methods).toContain('listViews'); - expect(DEFAULT_VIEW_ROUTES.methods).toContain('getView'); - expect(DEFAULT_VIEW_ROUTES.methods).toContain('createView'); - expect(DEFAULT_VIEW_ROUTES.methods).toContain('updateView'); - expect(DEFAULT_VIEW_ROUTES.methods).toContain('deleteView'); - expect(DEFAULT_VIEW_ROUTES.endpoints).toHaveLength(5); - }); - - it('should validate DEFAULT_WORKFLOW_ROUTES', () => { - expect(DEFAULT_WORKFLOW_ROUTES.prefix).toBe('/api/v1/workflow'); - expect(DEFAULT_WORKFLOW_ROUTES.service).toBe('workflow'); - expect(DEFAULT_WORKFLOW_ROUTES.category).toBe('workflow'); - expect(DEFAULT_WORKFLOW_ROUTES.methods).toContain('getWorkflowConfig'); - expect(DEFAULT_WORKFLOW_ROUTES.methods).toContain('getWorkflowState'); - expect(DEFAULT_WORKFLOW_ROUTES.methods).toContain('workflowTransition'); - // ADR-0019: approve/reject are no longer workflow routes (moved to /approvals). - expect(DEFAULT_WORKFLOW_ROUTES.methods).not.toContain('workflowApprove'); - expect(DEFAULT_WORKFLOW_ROUTES.methods).not.toContain('workflowReject'); - expect(DEFAULT_WORKFLOW_ROUTES.endpoints).toHaveLength(3); - }); - - it('should validate DEFAULT_REALTIME_ROUTES', () => { - expect(DEFAULT_REALTIME_ROUTES.prefix).toBe('/api/v1/realtime'); - expect(DEFAULT_REALTIME_ROUTES.service).toBe('realtime'); - expect(DEFAULT_REALTIME_ROUTES.category).toBe('realtime'); - expect(DEFAULT_REALTIME_ROUTES.methods).toContain('realtimeConnect'); - expect(DEFAULT_REALTIME_ROUTES.methods).toContain('realtimeDisconnect'); - expect(DEFAULT_REALTIME_ROUTES.methods).toContain('realtimeSubscribe'); - expect(DEFAULT_REALTIME_ROUTES.methods).toContain('realtimeUnsubscribe'); - expect(DEFAULT_REALTIME_ROUTES.methods).toContain('setPresence'); - expect(DEFAULT_REALTIME_ROUTES.methods).toContain('getPresence'); - expect(DEFAULT_REALTIME_ROUTES.endpoints).toHaveLength(6); - }); - it('should validate DEFAULT_NOTIFICATION_ROUTES', () => { expect(DEFAULT_NOTIFICATION_ROUTES.prefix).toBe('/api/v1/notifications'); expect(DEFAULT_NOTIFICATION_ROUTES.service).toBe('notification'); @@ -620,23 +568,21 @@ describe('plugin-rest-api.zod', () => { expect(actionsEndpoint?.responseSchema).toBe('AutomationActionsResponseSchema'); }); - it('should return all 13 default registrations', () => { + it('should return all 9 default registrations', () => { + // Permission/View/Workflow/Realtime tables were deleted in #3612 — + // no server ever mounted those routes. const registrations = getDefaultRouteRegistrations(); - - expect(registrations).toHaveLength(13); + + expect(registrations).toHaveLength(9); expect(registrations[0]).toBe(DEFAULT_DISCOVERY_ROUTES); expect(registrations[1]).toBe(DEFAULT_METADATA_ROUTES); expect(registrations[2]).toBe(DEFAULT_DATA_CRUD_ROUTES); expect(registrations[3]).toBe(DEFAULT_BATCH_ROUTES); - expect(registrations[4]).toBe(DEFAULT_PERMISSION_ROUTES); - expect(registrations[5]).toBe(DEFAULT_VIEW_ROUTES); - expect(registrations[6]).toBe(DEFAULT_WORKFLOW_ROUTES); - expect(registrations[7]).toBe(DEFAULT_REALTIME_ROUTES); - expect(registrations[8]).toBe(DEFAULT_NOTIFICATION_ROUTES); - expect(registrations[9]).toBe(DEFAULT_AI_ROUTES); - expect(registrations[10]).toBe(DEFAULT_I18N_ROUTES); - expect(registrations[11]).toBe(DEFAULT_ANALYTICS_ROUTES); - expect(registrations[12]).toBe(DEFAULT_AUTOMATION_ROUTES); + expect(registrations[4]).toBe(DEFAULT_NOTIFICATION_ROUTES); + expect(registrations[5]).toBe(DEFAULT_AI_ROUTES); + expect(registrations[6]).toBe(DEFAULT_I18N_ROUTES); + expect(registrations[7]).toBe(DEFAULT_ANALYTICS_ROUTES); + expect(registrations[8]).toBe(DEFAULT_AUTOMATION_ROUTES); }); it('should cover all protocol categories', () => { @@ -647,15 +593,15 @@ describe('plugin-rest-api.zod', () => { expect(categories).toContain('metadata'); expect(categories).toContain('data'); expect(categories).toContain('batch'); - expect(categories).toContain('permission'); - expect(categories).toContain('ui'); - expect(categories).toContain('workflow'); - expect(categories).toContain('realtime'); expect(categories).toContain('notification'); expect(categories).toContain('ai'); expect(categories).toContain('i18n'); expect(categories).toContain('analytics'); expect(categories).toContain('automation'); + // permission/ui/workflow/realtime categories left with their tables (#3612). + expect(categories).not.toContain('permission'); + expect(categories).not.toContain('workflow'); + expect(categories).not.toContain('realtime'); }); }); diff --git a/packages/spec/src/api/plugin-rest-api.zod.ts b/packages/spec/src/api/plugin-rest-api.zod.ts index 0115e6939a..fedf82524f 100644 --- a/packages/spec/src/api/plugin-rest-api.zod.ts +++ b/packages/spec/src/api/plugin-rest-api.zod.ts @@ -960,317 +960,10 @@ export const DEFAULT_BATCH_ROUTES: RestApiRouteRegistration = { ], }; -/** - * Default Permission Routes - * Standard routes for permission checking - */ -export const DEFAULT_PERMISSION_ROUTES: RestApiRouteRegistration = { - prefix: '/api/v1/auth', - service: 'auth', - category: 'permission', - methods: ['checkPermission', 'getObjectPermissions', 'getEffectivePermissions'], - authRequired: true, - endpoints: [ - { - method: 'POST', - path: '/check', - handler: 'checkPermission', - category: 'permission', - public: false, - summary: 'Check permission', - description: 'Check if current user has a specific permission', - tags: ['Permission'], - requestSchema: 'CheckPermissionRequestSchema', - responseSchema: 'CheckPermissionResponseSchema', - cacheable: false, - }, - { - method: 'GET', - path: '/permissions/:object', - handler: 'getObjectPermissions', - category: 'permission', - public: false, - summary: 'Get object permissions', - description: 'Get all permissions for a specific object', - tags: ['Permission'], - responseSchema: 'ObjectPermissionsResponseSchema', - cacheable: true, - cacheTtl: 300, - }, - { - method: 'GET', - path: '/permissions/effective', - handler: 'getEffectivePermissions', - category: 'permission', - public: false, - summary: 'Get effective permissions', - description: 'Get all effective permissions for current user', - tags: ['Permission'], - responseSchema: 'EffectivePermissionsResponseSchema', - cacheable: true, - cacheTtl: 300, - }, - ], - middleware: [ - { name: 'auth', type: 'authentication', enabled: true, order: 10 }, - { name: 'response_envelope', type: 'transformation', enabled: true, order: 100 }, - ], -}; - -// ========================================== -// View Management Routes -// ========================================== - -/** - * Default View Management Routes - * Standard routes for UI view CRUD operations - */ -export const DEFAULT_VIEW_ROUTES: RestApiRouteRegistration = { - prefix: '/api/v1/ui', - service: 'ui', - category: 'ui', - methods: ['listViews', 'getView', 'createView', 'updateView', 'deleteView'], - authRequired: true, - endpoints: [ - { - method: 'GET', - path: '/views/:object', - handler: 'listViews', - category: 'ui', - public: false, - summary: 'List views for an object', - description: 'Returns all views (list, form) for the specified object', - tags: ['Views', 'UI'], - responseSchema: 'ListViewsResponseSchema', - cacheable: true, - cacheTtl: 1800, - }, - { - method: 'GET', - path: '/views/:object/:viewId', - handler: 'getView', - category: 'ui', - public: false, - summary: 'Get a specific view', - description: 'Returns a specific view definition by object and view ID', - tags: ['Views', 'UI'], - responseSchema: 'GetViewResponseSchema', - cacheable: true, - cacheTtl: 1800, - }, - { - method: 'POST', - path: '/views/:object', - handler: 'createView', - category: 'ui', - public: false, - summary: 'Create a new view', - description: 'Creates a new view definition for the specified object', - tags: ['Views', 'UI'], - requestSchema: 'CreateViewRequestSchema', - responseSchema: 'CreateViewResponseSchema', - permissions: ['ui.view.create'], - cacheable: false, - }, - { - method: 'PATCH', - path: '/views/:object/:viewId', - handler: 'updateView', - category: 'ui', - public: false, - summary: 'Update a view', - description: 'Updates an existing view definition', - tags: ['Views', 'UI'], - requestSchema: 'UpdateViewRequestSchema', - responseSchema: 'UpdateViewResponseSchema', - permissions: ['ui.view.update'], - cacheable: false, - }, - { - method: 'DELETE', - path: '/views/:object/:viewId', - handler: 'deleteView', - category: 'ui', - public: false, - summary: 'Delete a view', - description: 'Deletes a view definition', - tags: ['Views', 'UI'], - responseSchema: 'DeleteViewResponseSchema', - permissions: ['ui.view.delete'], - cacheable: false, - }, - ], - middleware: [ - { name: 'auth', type: 'authentication', enabled: true, order: 10 }, - { name: 'validation', type: 'validation', enabled: true, order: 20 }, - { name: 'response_envelope', type: 'transformation', enabled: true, order: 100 }, - ], -}; - -// ========================================== -// Workflow Routes -// ========================================== - -/** - * Default Workflow Routes - * Standard routes for workflow state management and transitions - */ -export const DEFAULT_WORKFLOW_ROUTES: RestApiRouteRegistration = { - prefix: '/api/v1/workflow', - service: 'workflow', - category: 'workflow', - methods: ['getWorkflowConfig', 'getWorkflowState', 'workflowTransition'], - authRequired: true, - endpoints: [ - { - method: 'GET', - path: '/:object/config', - handler: 'getWorkflowConfig', - category: 'workflow', - public: false, - summary: 'Get workflow configuration', - description: 'Returns flow, approval-node, and state machine configuration for an object', - tags: ['Workflow'], - responseSchema: 'GetWorkflowConfigResponseSchema', - cacheable: true, - cacheTtl: 3600, - }, - { - method: 'GET', - path: '/:object/:recordId/state', - handler: 'getWorkflowState', - category: 'workflow', - public: false, - summary: 'Get workflow state', - description: 'Returns current workflow state and available transitions for a record', - tags: ['Workflow'], - responseSchema: 'GetWorkflowStateResponseSchema', - cacheable: false, - }, - { - method: 'POST', - path: '/:object/:recordId/transition', - handler: 'workflowTransition', - category: 'workflow', - public: false, - summary: 'Execute workflow transition', - description: 'Transitions a record to a new workflow state', - tags: ['Workflow'], - requestSchema: 'WorkflowTransitionRequestSchema', - responseSchema: 'WorkflowTransitionResponseSchema', - permissions: ['workflow.transition'], - cacheable: false, - }, - // ADR-0019: approve/reject are no longer workflow routes. Approval is a - // flow node; decisions are recorded on the approvals runtime via - // `POST /api/v1/approvals/requests/:id/{approve,reject}`. - ], - middleware: [ - { name: 'auth', type: 'authentication', enabled: true, order: 10 }, - { name: 'validation', type: 'validation', enabled: true, order: 20 }, - { name: 'response_envelope', type: 'transformation', enabled: true, order: 100 }, - { name: 'error_handler', type: 'error', enabled: true, order: 200 }, - ], -}; - -// ========================================== -// Realtime Routes -// ========================================== - -/** - * Default Realtime Routes - * Standard routes for realtime connection management and subscriptions - */ -export const DEFAULT_REALTIME_ROUTES: RestApiRouteRegistration = { - prefix: '/api/v1/realtime', - service: 'realtime', - category: 'realtime', - methods: ['realtimeConnect', 'realtimeDisconnect', 'realtimeSubscribe', 'realtimeUnsubscribe', 'setPresence', 'getPresence'], - authRequired: true, - endpoints: [ - { - method: 'POST', - path: '/connect', - handler: 'realtimeConnect', - category: 'realtime', - public: false, - summary: 'Establish realtime connection', - description: 'Negotiates a realtime connection (WebSocket/SSE) and returns connection details', - tags: ['Realtime'], - requestSchema: 'RealtimeConnectRequestSchema', - responseSchema: 'RealtimeConnectResponseSchema', - cacheable: false, - }, - { - method: 'POST', - path: '/disconnect', - handler: 'realtimeDisconnect', - category: 'realtime', - public: false, - summary: 'Close realtime connection', - description: 'Closes an active realtime connection', - tags: ['Realtime'], - requestSchema: 'RealtimeDisconnectRequestSchema', - responseSchema: 'RealtimeDisconnectResponseSchema', - cacheable: false, - }, - { - method: 'POST', - path: '/subscribe', - handler: 'realtimeSubscribe', - category: 'realtime', - public: false, - summary: 'Subscribe to channel', - description: 'Subscribes to a realtime channel for receiving events', - tags: ['Realtime'], - requestSchema: 'RealtimeSubscribeRequestSchema', - responseSchema: 'RealtimeSubscribeResponseSchema', - cacheable: false, - }, - { - method: 'POST', - path: '/unsubscribe', - handler: 'realtimeUnsubscribe', - category: 'realtime', - public: false, - summary: 'Unsubscribe from channel', - description: 'Unsubscribes from a realtime channel', - tags: ['Realtime'], - requestSchema: 'RealtimeUnsubscribeRequestSchema', - responseSchema: 'RealtimeUnsubscribeResponseSchema', - cacheable: false, - }, - { - method: 'PUT', - path: '/presence/:channel', - handler: 'setPresence', - category: 'realtime', - public: false, - summary: 'Set presence state', - description: 'Sets the current user\'s presence state in a channel', - tags: ['Realtime'], - requestSchema: 'SetPresenceRequestSchema', - responseSchema: 'SetPresenceResponseSchema', - cacheable: false, - }, - { - method: 'GET', - path: '/presence/:channel', - handler: 'getPresence', - category: 'realtime', - public: false, - summary: 'Get channel presence', - description: 'Returns all active members and their presence state in a channel', - tags: ['Realtime'], - responseSchema: 'GetPresenceResponseSchema', - cacheable: false, - }, - ], - middleware: [ - { name: 'auth', type: 'authentication', enabled: true, order: 10 }, - { name: 'response_envelope', type: 'transformation', enabled: true, order: 100 }, - ], -}; +// The Permission / View / Workflow / Realtime route tables were deleted +// (#3612): no server ever mounted those routes and nothing consumed the +// tables — they only underwrote dead client SDK namespaces (removed in the +// same change). Same disease, same cure as DEFAULT_DISPATCHER_ROUTES (#3586). // ========================================== // Notification Routes @@ -1648,15 +1341,11 @@ export const RestApiRouteRegistration = Object.assign(RestApiRouteRegistrationSc * 2. Metadata - Object/field schema CRUD * 3. Data CRUD - Record operations * 4. Batch - Bulk operations - * 5. Permission - Authorization checks - * 6. Views - UI view CRUD - * 7. Workflow - State machine transitions - * 8. Realtime - WebSocket/SSE connections - * 9. Notification - Push notifications and preferences - * 10. AI - NLQ, chat, suggestions, insights - * 11. i18n - Locales and translations - * 12. Analytics - BI queries and metadata - * 13. Automation - Trigger flows and scripts + * 5. Notification - Push notifications and preferences + * 6. AI - NLQ, chat, suggestions, insights + * 7. i18n - Locales and translations + * 8. Analytics - BI queries and metadata + * 9. Automation - Trigger flows and scripts */ export function getDefaultRouteRegistrations(): RestApiRouteRegistration[] { return [ @@ -1664,10 +1353,6 @@ export function getDefaultRouteRegistrations(): RestApiRouteRegistration[] { DEFAULT_METADATA_ROUTES, DEFAULT_DATA_CRUD_ROUTES, DEFAULT_BATCH_ROUTES, - DEFAULT_PERMISSION_ROUTES, - DEFAULT_VIEW_ROUTES, - DEFAULT_WORKFLOW_ROUTES, - DEFAULT_REALTIME_ROUTES, DEFAULT_NOTIFICATION_ROUTES, DEFAULT_AI_ROUTES, DEFAULT_I18N_ROUTES,