Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions .changeset/remove-dead-client-surfaces.md
Original file line number Diff line number Diff line change
@@ -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).
43 changes: 7 additions & 36 deletions content/docs/api/client-sdk.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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`) |
Expand All @@ -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.

<Callout type="info">
**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))
Expand Down Expand Up @@ -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
Expand All @@ -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();
Expand Down Expand Up @@ -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);
```

<Callout type="info">
**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.
</Callout>

---
Expand Down
162 changes: 14 additions & 148 deletions packages/client/CLIENT_SERVER_INTEGRATION_TESTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)**

---

Expand Down
Loading
Loading