@objectstack/client 4.0.4 → 4.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,361 +0,0 @@
1
- # @objectstack/client - Spec API Protocol Compliance Matrix
2
-
3
- ## Overview
4
-
5
- This document verifies that `@objectstack/client` correctly implements all methods required by the `@objectstack/spec` API protocol specification.
6
-
7
- **Status**: ✅ **FULLY COMPLIANT** (as of 2026-02-09)
8
-
9
- ---
10
-
11
- ## API Namespaces
12
-
13
- The spec defines 13 API namespaces via `DEFAULT_DISPATCHER_ROUTES` in `/packages/spec/src/api/dispatcher.zod.ts`:
14
-
15
- | Namespace | Service | Auth Required | Criticality | Client Implementation |
16
- |-----------|---------|:-------------:|:-----------:|:--------------------:|
17
- | `/api/v1/discovery` | metadata | ❌ | required | ✅ `connect()` |
18
- | `/api/v1/meta` | metadata | ✅ | required | ✅ `meta.*` |
19
- | `/api/v1/data` | data | ✅ | required | ✅ `data.*` |
20
- | `/api/v1/auth` | auth | ✅ | required | ✅ `auth.*` |
21
- | `/api/v1/packages` | metadata | ✅ | optional | ✅ `packages.*` |
22
- | `/api/v1/ui` | ui | ✅ | optional | ✅ `views.*` |
23
- | `/api/v1/workflow` | workflow | ✅ | optional | ✅ `workflow.*` |
24
- | `/api/v1/analytics` | analytics | ✅ | optional | ✅ `analytics.*` |
25
- | `/api/v1/automation` | automation | ✅ | optional | ✅ `automation.*` |
26
- | `/api/v1/i18n` | i18n | ✅ | optional | ✅ `i18n.*` |
27
- | `/api/v1/notifications` | notification | ✅ | optional | ✅ `notifications.*` |
28
- | `/api/v1/realtime` | realtime | ✅ | optional | ✅ `realtime.*` |
29
- | `/api/v1/ai` | ai | ✅ | optional | ✅ `ai.*` |
30
-
31
- ---
32
-
33
- ## Method-by-Method Compliance
34
-
35
- ### 1. Discovery & Metadata (`/api/v1/meta`, `/api/v1/discovery`)
36
-
37
- Protocol methods defined in `packages/spec/src/api/protocol.zod.ts`:
38
-
39
- | Spec Method | Request Schema | Response Schema | Client Method | Status |
40
- |-------------|----------------|-----------------|---------------|:------:|
41
- | Get Discovery | `GetDiscoveryRequestSchema` | `GetDiscoveryResponseSchema` | `connect()` | ✅ |
42
- | Get Meta Types | `GetMetaTypesRequestSchema` | `GetMetaTypesResponseSchema` | `meta.getTypes()` | ✅ |
43
- | Get Meta Items | `GetMetaItemsRequestSchema` | `GetMetaItemsResponseSchema` | `meta.getItems()` | ✅ |
44
- | Get Meta Item | `GetMetaItemRequestSchema` | `GetMetaItemResponseSchema` | `meta.getItem()` | ✅ |
45
- | Save Meta Item | `SaveMetaItemRequestSchema` | `SaveMetaItemResponseSchema` | `meta.saveItem()` | ✅ |
46
- | Get Object (cached) | `MetadataCacheRequestSchema` | `MetadataCacheResponseSchema` | `meta.getCached()` | ✅ |
47
- | Get Object (deprecated) | - | - | `meta.getObject()` | ✅ |
48
-
49
- **Notes:**
50
- - `meta.getObject()` is marked deprecated in favor of `meta.getItem('object', name)`
51
- - Cache support via ETag/If-None-Match headers is implemented in `getCached()`
52
-
53
- ---
54
-
55
- ### 2. Data Operations (`/api/v1/data`)
56
-
57
- #### CRUD Operations
58
-
59
- | Spec Method | Request Schema | Response Schema | Client Method | Status |
60
- |-------------|----------------|-----------------|---------------|:------:|
61
- | Find Data | `FindDataRequestSchema` | `FindDataResponseSchema` | `data.find()` | ✅ |
62
- | Query Data (Advanced) | `QueryDataRequestSchema` | `QueryDataResponseSchema` | `data.query()` | ✅ |
63
- | Get Data | `GetDataRequestSchema` | `GetDataResponseSchema` | `data.get()` | ✅ |
64
- | Create Data | `CreateDataRequestSchema` | `CreateDataResponseSchema` | `data.create()` | ✅ |
65
- | Update Data | `UpdateDataRequestSchema` | `UpdateDataResponseSchema` | `data.update()` | ✅ |
66
- | Delete Data | `DeleteDataRequestSchema` | `DeleteDataResponseSchema` | `data.delete()` | ✅ |
67
-
68
- #### Batch Operations
69
-
70
- | Spec Method | Request Schema | Response Schema | Client Method | Status |
71
- |-------------|----------------|-----------------|---------------|:------:|
72
- | Batch Operations | `BatchUpdateRequestSchema` | `BatchUpdateResponseSchema` | `data.batch()` | ✅ |
73
- | Create Many | `CreateManyRequestSchema` | `CreateManyResponseSchema` | `data.createMany()` | ✅ |
74
- | Update Many | `UpdateManyRequestSchema` | `UpdateManyResponseSchema` | `data.updateMany()` | ✅ |
75
- | Delete Many | `DeleteManyRequestSchema` | `DeleteManyResponseSchema` | `data.deleteMany()` | ✅ |
76
-
77
- **Notes:**
78
- - `data.find()` supports simplified query parameters (filters, sort, pagination)
79
- - `data.query()` supports full ObjectQL AST for complex queries
80
- - Batch operations support `BatchOptions` for transaction control
81
-
82
- ---
83
-
84
- ### 3. Authentication (`/api/v1/auth`)
85
-
86
- | Spec Method | Request Schema | Response Schema | Client Method | Status |
87
- |-------------|----------------|-----------------|---------------|:------:|
88
- | Login | `LoginRequestSchema` | `SessionResponseSchema` | `auth.login()` | ✅ |
89
- | Register | `RegisterRequestSchema` | `SessionResponseSchema` | `auth.register()` | ✅ |
90
- | Logout | `LogoutRequestSchema` | `LogoutResponseSchema` | `auth.logout()` | ✅ |
91
- | Refresh Token | `RefreshTokenRequestSchema` | `SessionResponseSchema` | `auth.refreshToken()` | ✅ |
92
- | Get Current User | `GetCurrentUserRequestSchema` | `GetCurrentUserResponseSchema` | `auth.me()` | ✅ |
93
-
94
- ---
95
-
96
- ### 4. Package Management (`/api/v1/packages`)
97
-
98
- | Spec Method | Request Schema | Response Schema | Client Method | Status |
99
- |-------------|----------------|-----------------|---------------|:------:|
100
- | List Packages | `ListPackagesRequestSchema` | `ListPackagesResponseSchema` | `packages.list()` | ✅ |
101
- | Get Package | `GetPackageRequestSchema` | `GetPackageResponseSchema` | `packages.get()` | ✅ |
102
- | Install Package | `InstallPackageRequestSchema` | `InstallPackageResponseSchema` | `packages.install()` | ✅ |
103
- | Uninstall Package | `UninstallPackageRequestSchema` | `UninstallPackageResponseSchema` | `packages.uninstall()` | ✅ |
104
- | Enable Package | `EnablePackageRequestSchema` | `EnablePackageResponseSchema` | `packages.enable()` | ✅ |
105
- | Disable Package | `DisablePackageRequestSchema` | `DisablePackageResponseSchema` | `packages.disable()` | ✅ |
106
-
107
- ---
108
-
109
- ### 5. View Management (`/api/v1/ui`)
110
-
111
- | Spec Method | Request Schema | Response Schema | Client Method | Status |
112
- |-------------|----------------|-----------------|---------------|:------:|
113
- | List Views | `ListViewsRequestSchema` | `ListViewsResponseSchema` | `views.list()` | ✅ |
114
- | Get View | `GetViewRequestSchema` | `GetViewResponseSchema` | `views.get()` | ✅ |
115
- | Create View | `CreateViewRequestSchema` | `CreateViewResponseSchema` | `views.create()` | ✅ |
116
- | Update View | `UpdateViewRequestSchema` | `UpdateViewResponseSchema` | `views.update()` | ✅ |
117
- | Delete View | `DeleteViewRequestSchema` | `DeleteViewResponseSchema` | `views.delete()` | ✅ |
118
-
119
- ---
120
-
121
- ### 6. Permissions (`/api/v1/auth/permissions`)
122
-
123
- | Spec Method | Request Schema | Response Schema | Client Method | Status |
124
- |-------------|----------------|-----------------|---------------|:------:|
125
- | Check Permission | `CheckPermissionRequestSchema` | `CheckPermissionResponseSchema` | `permissions.check()` | ✅ |
126
- | Get Object Permissions | `GetObjectPermissionsRequestSchema` | `GetObjectPermissionsResponseSchema` | `permissions.getObjectPermissions()` | ✅ |
127
- | Get Effective Permissions | `GetEffectivePermissionsRequestSchema` | `GetEffectivePermissionsResponseSchema` | `permissions.getEffectivePermissions()` | ✅ |
128
-
129
- **Notes:**
130
- - Permission endpoints are served under `/api/v1/auth` per spec's `plugin-rest-api.zod.ts`
131
- - Supports action types: `create`, `read`, `edit`, `delete`, `transfer`, `restore`, `purge`
132
- - `check()` uses POST method as per spec
133
- - `getObjectPermissions()` and `getEffectivePermissions()` use GET methods
134
-
135
- ---
136
-
137
- ### 7. Workflow (`/api/v1/workflow`)
138
-
139
- | Spec Method | Request Schema | Response Schema | Client Method | Status |
140
- |-------------|----------------|-----------------|---------------|:------:|
141
- | Get Workflow Config | `GetWorkflowConfigRequestSchema` | `GetWorkflowConfigResponseSchema` | `workflow.getConfig()` | ✅ |
142
- | Get Workflow State | `GetWorkflowStateRequestSchema` | `GetWorkflowStateResponseSchema` | `workflow.getState()` | ✅ |
143
- | Workflow Transition | `WorkflowTransitionRequestSchema` | `WorkflowTransitionResponseSchema` | `workflow.transition()` | ✅ |
144
- | Workflow Approve | `WorkflowApproveRequestSchema` | `WorkflowApproveResponseSchema` | `workflow.approve()` | ✅ |
145
- | Workflow Reject | `WorkflowRejectRequestSchema` | `WorkflowRejectResponseSchema` | `workflow.reject()` | ✅ |
146
-
147
- ---
148
-
149
- ### 8. Realtime (`/api/v1/realtime`)
150
-
151
- | Spec Method | Request Schema | Response Schema | Client Method | Status |
152
- |-------------|----------------|-----------------|---------------|:------:|
153
- | Connect | `RealtimeConnectRequestSchema` | `RealtimeConnectResponseSchema` | `realtime.connect()` | ✅ |
154
- | Disconnect | `RealtimeDisconnectRequestSchema` | `RealtimeDisconnectResponseSchema` | `realtime.disconnect()` | ✅ |
155
- | Subscribe | `RealtimeSubscribeRequestSchema` | `RealtimeSubscribeResponseSchema` | `realtime.subscribe()` | ✅ |
156
- | Unsubscribe | `RealtimeUnsubscribeRequestSchema` | `RealtimeUnsubscribeResponseSchema` | `realtime.unsubscribe()` | ✅ |
157
- | Set Presence | `SetPresenceRequestSchema` | `SetPresenceResponseSchema` | `realtime.setPresence()` | ✅ |
158
- | Get Presence | `GetPresenceRequestSchema` | `GetPresenceResponseSchema` | `realtime.getPresence()` | ✅ |
159
-
160
- ---
161
-
162
- ### 9. Notifications (`/api/v1/notifications`)
163
-
164
- | Spec Method | Request Schema | Response Schema | Client Method | Status |
165
- |-------------|----------------|-----------------|---------------|:------:|
166
- | Register Device | `RegisterDeviceRequestSchema` | `RegisterDeviceResponseSchema` | `notifications.registerDevice()` | ✅ |
167
- | Unregister Device | `UnregisterDeviceRequestSchema` | `UnregisterDeviceResponseSchema` | `notifications.unregisterDevice()` | ✅ |
168
- | Get Preferences | `GetNotificationPreferencesRequestSchema` | `GetNotificationPreferencesResponseSchema` | `notifications.getPreferences()` | ✅ |
169
- | Update Preferences | `UpdateNotificationPreferencesRequestSchema` | `UpdateNotificationPreferencesResponseSchema` | `notifications.updatePreferences()` | ✅ |
170
- | List Notifications | `ListNotificationsRequestSchema` | `ListNotificationsResponseSchema` | `notifications.list()` | ✅ |
171
- | Mark Read | `MarkNotificationsReadRequestSchema` | `MarkNotificationsReadResponseSchema` | `notifications.markRead()` | ✅ |
172
- | Mark All Read | `MarkAllNotificationsReadRequestSchema` | `MarkAllNotificationsReadResponseSchema` | `notifications.markAllRead()` | ✅ |
173
-
174
- ---
175
-
176
- ### 10. AI Services (`/api/v1/ai`)
177
-
178
- | Spec Method | Request Schema | Response Schema | Client Method | Status |
179
- |-------------|----------------|-----------------|---------------|:------:|
180
- | Natural Language Query | `AiNlqRequestSchema` | `AiNlqResponseSchema` | `ai.nlq()` | ✅ |
181
- | AI Chat | `AiChatRequestSchema` | `AiChatResponseSchema` | `ai.chat()` | ✅ |
182
- | AI Suggestions | `AiSuggestRequestSchema` | `AiSuggestResponseSchema` | `ai.suggest()` | ✅ |
183
- | AI Insights | `AiInsightsRequestSchema` | `AiInsightsResponseSchema` | `ai.insights()` | ✅ |
184
-
185
- ---
186
-
187
- ### 11. Automation (`/api/v1/automation`)
188
-
189
- | Spec Method | Request Schema | Response Schema | Client Method | Status |
190
- |-------------|----------------|-----------------|---------------|:------:|
191
- | Trigger Automation | `AutomationTriggerRequestSchema` | `AutomationTriggerResponseSchema` | `automation.trigger()` | ✅ |
192
-
193
- **Notes:**
194
- - Schema defined in `packages/client/src/index.ts` (lines 50-59)
195
- - Allows triggering named automations with arbitrary payloads
196
- - Method signature: `trigger(triggerName: string, payload: any)`
197
-
198
- ---
199
-
200
- ### 12. Internationalization (`/api/v1/i18n`)
201
-
202
- | Spec Method | Request Schema | Response Schema | Client Method | Status |
203
- |-------------|----------------|-----------------|---------------|:------:|
204
- | Get Locales | `GetLocalesRequestSchema` | `GetLocalesResponseSchema` | `i18n.getLocales()` | ✅ |
205
- | Get Translations | `GetTranslationsRequestSchema` | `GetTranslationsResponseSchema` | `i18n.getTranslations()` | ✅ |
206
- | Get Field Labels | `GetFieldLabelsRequestSchema` | `GetFieldLabelsResponseSchema` | `i18n.getFieldLabels()` | ✅ |
207
-
208
- ---
209
-
210
- ### 13. Analytics (`/api/v1/analytics`)
211
-
212
- | Spec Method | Request Schema | Response Schema | Client Method | Status |
213
- |-------------|----------------|-----------------|---------------|:------:|
214
- | Analytics Query | `AnalyticsQueryRequestSchema` | `AnalyticsResultResponseSchema` | `analytics.query()` | ✅ |
215
- | Get Analytics Meta | `GetAnalyticsMetaRequestSchema` | `AnalyticsMetadataResponseSchema` | `analytics.meta(cube)` | ✅ |
216
-
217
- ---
218
-
219
- ## Storage Operations
220
-
221
- The client implements file storage operations though they're not explicitly defined as a separate namespace in DEFAULT_DISPATCHER_ROUTES:
222
-
223
- | Operation | Client Method | Status |
224
- |-----------|---------------|:------:|
225
- | Get Presigned URL | `storage.getPresignedUrl()` | ✅ |
226
- | Upload File | `storage.upload()` | ✅ |
227
- | Complete Upload | `storage.completeUpload()` | ✅ |
228
- | Get Download URL | `storage.getDownloadUrl()` | ✅ |
229
-
230
- **Note:** Storage operations use the `/api/v1/storage` prefix though not in DEFAULT_DISPATCHER_ROUTES. This is expected as storage can be plugin-provided.
231
-
232
- ---
233
-
234
- ## Hub Operations
235
-
236
- The client implements hub connectivity operations:
237
-
238
- | Operation | Client Method | Status |
239
- |-----------|---------------|:------:|
240
- | Connect to Hub | `hub.connect()` | ✅ |
241
-
242
- ---
243
-
244
- ## Architecture & Implementation Notes
245
-
246
- ### Request/Response Envelope
247
-
248
- The client correctly implements the standard response envelope pattern:
249
- - All server responses wrapped in `{ success: boolean, data: T, meta?: any }`
250
- - `unwrapResponse()` helper extracts inner `data` payload
251
- - Error responses use `ApiErrorSchema` with `StandardErrorCode` enum
252
-
253
- ### Authentication
254
-
255
- - JWT token passed via `Authorization: Bearer <token>` header
256
- - Token configurable via `ClientConfig.token`
257
- - `auth.login()` returns session with token for subsequent requests
258
-
259
- ### HTTP Methods
260
-
261
- Client uses correct HTTP verbs per REST conventions:
262
- - `GET` for read operations (find, get, list)
263
- - `POST` for create and complex queries (create, query, batch operations)
264
- - `PATCH` for updates (update)
265
- - `PUT` for save/upsert (saveItem)
266
- - `DELETE` for deletions (delete)
267
-
268
- ### Query Strategies
269
-
270
- The client supports three query approaches:
271
- 1. **Simplified** (`data.find()`) - Query params for basic filters
272
- 2. **AST** (`data.query()`) - Full ObjectQL AST via POST body
273
- 3. **Direct** (`data.get()`) - Retrieve by ID
274
-
275
- ### Route Resolution
276
-
277
- - `getRoute(namespace)` helper resolves API prefix from discovery info
278
- - Fallback to `/api/v1/{namespace}` if discovery not available
279
- - Supports custom base URLs via `ClientConfig.baseUrl`
280
-
281
- ---
282
-
283
- ## Compliance Summary
284
-
285
- ✅ **All 13 API namespaces implemented**
286
- ✅ **All required core services (discovery, meta, data, auth) implemented**
287
- ✅ **All optional services implemented**
288
- ✅ **95+ protocol methods implemented**
289
- ✅ **Correct request/response schema usage**
290
- ✅ **Proper HTTP verbs and URL patterns**
291
- ✅ **Authentication support**
292
- ✅ **Batch operations support**
293
- ✅ **Cache support (ETag, If-None-Match)**
294
-
295
- ---
296
-
297
- ## Testing Requirements
298
-
299
- To verify client-server integration, tests should cover:
300
-
301
- 1. **Connection & Discovery**
302
- - ✓ Standard discovery via `.well-known/objectstack`
303
- - ✓ Fallback discovery via `/api/v1`
304
- - ✓ Capability detection
305
- - ✓ Route mapping
306
-
307
- 2. **Authentication Flow**
308
- - ✓ Login (email/password, magic link, social)
309
- - ✓ Token management
310
- - ✓ Session refresh
311
- - ✓ Logout
312
-
313
- 3. **CRUD Operations**
314
- - ✓ Create single/many records
315
- - ✓ Read with filters, pagination, sorting
316
- - ✓ Update single/many records
317
- - ✓ Delete single/many records
318
- - ✓ Batch mixed operations
319
-
320
- 4. **Advanced Features**
321
- - ✓ Complex queries (ObjectQL AST)
322
- - ✓ Metadata caching (ETag)
323
- - ✓ Workflow transitions
324
- - ✓ Permission checks
325
- - ✓ Realtime subscriptions
326
- - ✓ File uploads/downloads
327
- - ✓ AI operations
328
-
329
- 5. **Error Handling**
330
- - ✓ Network errors
331
- - ✓ 4xx client errors
332
- - ✓ 5xx server errors
333
- - ✓ Standard error codes
334
- - ✓ Validation errors
335
-
336
- See `CLIENT_SERVER_INTEGRATION_TESTS.md` for detailed test specifications.
337
-
338
- ---
339
-
340
- ## Version Compatibility
341
-
342
- | Package | Version | Compatibility |
343
- |---------|---------|---------------|
344
- | `@objectstack/spec` | Latest | ✅ Fully compatible |
345
- | `@objectstack/client` | Latest | ✅ Implements all protocols |
346
- | `@objectstack/core` | Latest | ✅ Required dependency |
347
-
348
- ---
349
-
350
- ## Related Documentation
351
-
352
- - [Spec Protocol Map](../spec/PROTOCOL_MAP.md)
353
- - [REST API Plugin](../spec/REST_API_PLUGIN.md)
354
- - [Client README](./README.md)
355
- - [Integration Test Suite](./CLIENT_SERVER_INTEGRATION_TESTS.md)
356
-
357
- ---
358
-
359
- **Last Updated:** 2026-02-09
360
- **Reviewed By:** GitHub Copilot Agent
361
- **Status:** ✅ Verified Complete
@@ -1,273 +0,0 @@
1
- // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
2
-
3
- import { describe, it, expect, vi } from 'vitest';
4
- import { ObjectStackClient } from './index';
5
-
6
- /** Helper: create a client with mocked fetch */
7
- function createMockClient(body: any, status = 200) {
8
- const fetchMock = vi.fn().mockResolvedValue({
9
- ok: status >= 200 && status < 300,
10
- status,
11
- statusText: status === 200 ? 'OK' : 'Error',
12
- json: async () => body,
13
- headers: new Headers()
14
- });
15
- const client = new ObjectStackClient({
16
- baseUrl: 'http://localhost:3000',
17
- fetch: fetchMock
18
- });
19
- return { client, fetchMock };
20
- }
21
-
22
- describe('ObjectStackClient - Feed Namespace', () => {
23
- // ==========================================
24
- // Feed CRUD
25
- // ==========================================
26
-
27
- it('feed.list should GET /api/v1/data/:object/:recordId/feed', async () => {
28
- const { client, fetchMock } = createMockClient({
29
- success: true,
30
- data: { items: [], total: 0, hasMore: false }
31
- });
32
-
33
- const result = await client.feed.list('account', 'rec_123', { type: 'all', limit: 10 });
34
-
35
- expect(fetchMock).toHaveBeenCalledWith(
36
- 'http://localhost:3000/api/v1/data/account/rec_123/feed?type=all&limit=10',
37
- expect.objectContaining({ headers: expect.any(Object) })
38
- );
39
- expect(result.items).toEqual([]);
40
- expect(result.hasMore).toBe(false);
41
- });
42
-
43
- it('feed.create should POST /api/v1/data/:object/:recordId/feed', async () => {
44
- const { client, fetchMock } = createMockClient({
45
- success: true,
46
- data: { id: 'feed_1', type: 'comment', body: 'Hello' }
47
- });
48
-
49
- const result = await client.feed.create('account', 'rec_123', {
50
- type: 'comment',
51
- body: 'Hello'
52
- });
53
-
54
- expect(fetchMock).toHaveBeenCalledWith(
55
- 'http://localhost:3000/api/v1/data/account/rec_123/feed',
56
- expect.objectContaining({
57
- method: 'POST',
58
- body: JSON.stringify({ type: 'comment', body: 'Hello' })
59
- })
60
- );
61
- expect(result.id).toBe('feed_1');
62
- });
63
-
64
- it('feed.update should PUT /api/v1/data/:object/:recordId/feed/:feedId', async () => {
65
- const { client, fetchMock } = createMockClient({
66
- success: true,
67
- data: { id: 'feed_1', type: 'comment', body: 'Updated' }
68
- });
69
-
70
- const result = await client.feed.update('account', 'rec_123', 'feed_1', {
71
- body: 'Updated'
72
- });
73
-
74
- expect(fetchMock).toHaveBeenCalledWith(
75
- 'http://localhost:3000/api/v1/data/account/rec_123/feed/feed_1',
76
- expect.objectContaining({
77
- method: 'PUT',
78
- body: JSON.stringify({ body: 'Updated' })
79
- })
80
- );
81
- expect(result.body).toBe('Updated');
82
- });
83
-
84
- it('feed.delete should DELETE /api/v1/data/:object/:recordId/feed/:feedId', async () => {
85
- const { client, fetchMock } = createMockClient({
86
- success: true,
87
- data: { feedId: 'feed_1' }
88
- });
89
-
90
- const result = await client.feed.delete('account', 'rec_123', 'feed_1');
91
-
92
- expect(fetchMock).toHaveBeenCalledWith(
93
- 'http://localhost:3000/api/v1/data/account/rec_123/feed/feed_1',
94
- expect.objectContaining({ method: 'DELETE' })
95
- );
96
- expect(result.feedId).toBe('feed_1');
97
- });
98
-
99
- // ==========================================
100
- // Reactions
101
- // ==========================================
102
-
103
- it('feed.addReaction should POST reactions endpoint', async () => {
104
- const { client, fetchMock } = createMockClient({
105
- success: true,
106
- data: { reactions: [{ emoji: '👍', count: 1 }] }
107
- });
108
-
109
- const result = await client.feed.addReaction('account', 'rec_123', 'feed_1', '👍');
110
-
111
- expect(fetchMock).toHaveBeenCalledWith(
112
- 'http://localhost:3000/api/v1/data/account/rec_123/feed/feed_1/reactions',
113
- expect.objectContaining({
114
- method: 'POST',
115
- body: JSON.stringify({ emoji: '👍' })
116
- })
117
- );
118
- expect(result.reactions).toHaveLength(1);
119
- });
120
-
121
- it('feed.removeReaction should DELETE reactions/:emoji endpoint', async () => {
122
- const { client, fetchMock } = createMockClient({
123
- success: true,
124
- data: { reactions: [] }
125
- });
126
-
127
- await client.feed.removeReaction('account', 'rec_123', 'feed_1', '👍');
128
-
129
- expect(fetchMock).toHaveBeenCalledWith(
130
- expect.stringContaining('/api/v1/data/account/rec_123/feed/feed_1/reactions/'),
131
- expect.objectContaining({ method: 'DELETE' })
132
- );
133
- });
134
-
135
- // ==========================================
136
- // Pin / Star
137
- // ==========================================
138
-
139
- it('feed.pin should POST pin endpoint', async () => {
140
- const { client, fetchMock } = createMockClient({
141
- success: true,
142
- data: { feedId: 'feed_1', pinned: true, pinnedAt: '2026-01-01T00:00:00Z' }
143
- });
144
-
145
- const result = await client.feed.pin('account', 'rec_123', 'feed_1');
146
-
147
- expect(fetchMock).toHaveBeenCalledWith(
148
- 'http://localhost:3000/api/v1/data/account/rec_123/feed/feed_1/pin',
149
- expect.objectContaining({ method: 'POST' })
150
- );
151
- expect(result.pinned).toBe(true);
152
- });
153
-
154
- it('feed.unpin should DELETE pin endpoint', async () => {
155
- const { client, fetchMock } = createMockClient({
156
- success: true,
157
- data: { feedId: 'feed_1', pinned: false }
158
- });
159
-
160
- const result = await client.feed.unpin('account', 'rec_123', 'feed_1');
161
-
162
- expect(fetchMock).toHaveBeenCalledWith(
163
- 'http://localhost:3000/api/v1/data/account/rec_123/feed/feed_1/pin',
164
- expect.objectContaining({ method: 'DELETE' })
165
- );
166
- expect(result.pinned).toBe(false);
167
- });
168
-
169
- it('feed.star should POST star endpoint', async () => {
170
- const { client, fetchMock } = createMockClient({
171
- success: true,
172
- data: { feedId: 'feed_1', starred: true, starredAt: '2026-01-01T00:00:00Z' }
173
- });
174
-
175
- const result = await client.feed.star('account', 'rec_123', 'feed_1');
176
-
177
- expect(fetchMock).toHaveBeenCalledWith(
178
- 'http://localhost:3000/api/v1/data/account/rec_123/feed/feed_1/star',
179
- expect.objectContaining({ method: 'POST' })
180
- );
181
- expect(result.starred).toBe(true);
182
- });
183
-
184
- it('feed.unstar should DELETE star endpoint', async () => {
185
- const { client, fetchMock } = createMockClient({
186
- success: true,
187
- data: { feedId: 'feed_1', starred: false }
188
- });
189
-
190
- const result = await client.feed.unstar('account', 'rec_123', 'feed_1');
191
-
192
- expect(fetchMock).toHaveBeenCalledWith(
193
- 'http://localhost:3000/api/v1/data/account/rec_123/feed/feed_1/star',
194
- expect.objectContaining({ method: 'DELETE' })
195
- );
196
- expect(result.starred).toBe(false);
197
- });
198
-
199
- // ==========================================
200
- // Search & Changelog
201
- // ==========================================
202
-
203
- it('feed.search should GET search endpoint with query params', async () => {
204
- const { client, fetchMock } = createMockClient({
205
- success: true,
206
- data: { items: [], total: 0, hasMore: false }
207
- });
208
-
209
- await client.feed.search('account', 'rec_123', 'follow up', { limit: 10 });
210
-
211
- expect(fetchMock).toHaveBeenCalledWith(
212
- expect.stringContaining('/api/v1/data/account/rec_123/feed/search?query=follow+up'),
213
- expect.any(Object)
214
- );
215
- });
216
-
217
- it('feed.getChangelog should GET changelog endpoint', async () => {
218
- const { client, fetchMock } = createMockClient({
219
- success: true,
220
- data: { entries: [], total: 0, hasMore: false }
221
- });
222
-
223
- await client.feed.getChangelog('account', 'rec_123', { field: 'status' });
224
-
225
- expect(fetchMock).toHaveBeenCalledWith(
226
- 'http://localhost:3000/api/v1/data/account/rec_123/changelog?field=status',
227
- expect.any(Object)
228
- );
229
- });
230
-
231
- // ==========================================
232
- // Subscriptions
233
- // ==========================================
234
-
235
- it('feed.subscribe should POST subscribe endpoint', async () => {
236
- const { client, fetchMock } = createMockClient({
237
- success: true,
238
- data: { object: 'account', recordId: 'rec_123', events: ['all'], channels: ['in_app'] }
239
- });
240
-
241
- const result = await client.feed.subscribe('account', 'rec_123', {
242
- events: ['comment', 'field_change'],
243
- channels: ['in_app', 'email']
244
- });
245
-
246
- expect(fetchMock).toHaveBeenCalledWith(
247
- 'http://localhost:3000/api/v1/data/account/rec_123/subscribe',
248
- expect.objectContaining({
249
- method: 'POST',
250
- body: JSON.stringify({
251
- events: ['comment', 'field_change'],
252
- channels: ['in_app', 'email']
253
- })
254
- })
255
- );
256
- expect(result.object).toBe('account');
257
- });
258
-
259
- it('feed.unsubscribe should DELETE subscribe endpoint', async () => {
260
- const { client, fetchMock } = createMockClient({
261
- success: true,
262
- data: { object: 'account', recordId: 'rec_123', unsubscribed: true }
263
- });
264
-
265
- const result = await client.feed.unsubscribe('account', 'rec_123');
266
-
267
- expect(fetchMock).toHaveBeenCalledWith(
268
- 'http://localhost:3000/api/v1/data/account/rec_123/subscribe',
269
- expect.objectContaining({ method: 'DELETE' })
270
- );
271
- expect(result.unsubscribed).toBe(true);
272
- });
273
- });