@objectstack/objectql 4.0.4 → 4.0.5

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,303 +0,0 @@
1
- // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
2
-
3
- import { describe, it, expect, beforeEach, vi } from 'vitest';
4
- import { ObjectStackProtocolImplementation } from './protocol.js';
5
- import { ObjectQL } from './engine.js';
6
- import type { IFeedService } from '@objectstack/spec/contracts';
7
-
8
- /**
9
- * Mock IFeedService for testing feed route handlers.
10
- */
11
- function createMockFeedService(): IFeedService {
12
- return {
13
- listFeed: vi.fn().mockResolvedValue({
14
- items: [{ id: 'feed_1', type: 'comment', body: 'Hello world', createdAt: '2026-01-01T00:00:00Z' }],
15
- total: 1,
16
- hasMore: false,
17
- }),
18
- createFeedItem: vi.fn().mockResolvedValue({
19
- id: 'feed_new',
20
- type: 'comment',
21
- body: 'New comment',
22
- createdAt: '2026-01-01T00:00:00Z',
23
- }),
24
- updateFeedItem: vi.fn().mockResolvedValue({
25
- id: 'feed_1',
26
- type: 'comment',
27
- body: 'Updated comment',
28
- createdAt: '2026-01-01T00:00:00Z',
29
- }),
30
- deleteFeedItem: vi.fn().mockResolvedValue(undefined),
31
- getFeedItem: vi.fn().mockResolvedValue({
32
- id: 'feed_1',
33
- type: 'comment',
34
- body: 'Hello world',
35
- createdAt: '2026-01-01T00:00:00Z',
36
- }),
37
- addReaction: vi.fn().mockResolvedValue([
38
- { emoji: '👍', users: ['current_user'], count: 1 },
39
- ]),
40
- removeReaction: vi.fn().mockResolvedValue([]),
41
- subscribe: vi.fn().mockResolvedValue({
42
- id: 'sub_1',
43
- object: 'account',
44
- recordId: 'rec_123',
45
- userId: 'current_user',
46
- events: ['all'],
47
- channels: ['in_app'],
48
- }),
49
- unsubscribe: vi.fn().mockResolvedValue(true),
50
- getSubscription: vi.fn().mockResolvedValue(null),
51
- };
52
- }
53
-
54
- describe('ObjectStackProtocolImplementation - Feed Operations', () => {
55
- let protocol: ObjectStackProtocolImplementation;
56
- let engine: ObjectQL;
57
- let feedService: IFeedService;
58
-
59
- beforeEach(() => {
60
- engine = new ObjectQL();
61
- feedService = createMockFeedService();
62
- protocol = new ObjectStackProtocolImplementation(engine, undefined, () => feedService);
63
- });
64
-
65
- // ==========================================
66
- // Discovery
67
- // ==========================================
68
-
69
- it('should show feed service as unavailable when not registered', async () => {
70
- const protocolNoFeed = new ObjectStackProtocolImplementation(engine);
71
- const discovery = await protocolNoFeed.getDiscovery();
72
-
73
- expect(discovery.services.feed).toBeDefined();
74
- expect(discovery.services.feed.enabled).toBe(false);
75
- expect(discovery.services.feed.status).toBe('unavailable');
76
- });
77
-
78
- it('should show feed service as available when registered', async () => {
79
- const mockServices = new Map<string, any>();
80
- mockServices.set('feed', {});
81
- const protocolWithFeed = new ObjectStackProtocolImplementation(engine, () => mockServices, () => feedService);
82
- const discovery = await protocolWithFeed.getDiscovery();
83
-
84
- expect(discovery.services.feed).toBeDefined();
85
- expect(discovery.services.feed.enabled).toBe(true);
86
- expect(discovery.services.feed.status).toBe('available');
87
- });
88
-
89
- // ==========================================
90
- // Feed CRUD
91
- // ==========================================
92
-
93
- it('listFeed should delegate to feedService.listFeed', async () => {
94
- const result = await protocol.listFeed({ object: 'account', recordId: 'rec_123' });
95
-
96
- expect(result.success).toBe(true);
97
- expect(result.data.items).toHaveLength(1);
98
- expect(feedService.listFeed).toHaveBeenCalledWith(
99
- expect.objectContaining({ object: 'account', recordId: 'rec_123' })
100
- );
101
- });
102
-
103
- it('createFeedItem should delegate to feedService.createFeedItem', async () => {
104
- const result = await protocol.createFeedItem({
105
- object: 'account',
106
- recordId: 'rec_123',
107
- type: 'comment',
108
- body: 'New comment',
109
- });
110
-
111
- expect(result.success).toBe(true);
112
- expect(result.data.id).toBe('feed_new');
113
- expect(feedService.createFeedItem).toHaveBeenCalledWith(
114
- expect.objectContaining({ object: 'account', recordId: 'rec_123', type: 'comment', body: 'New comment' })
115
- );
116
- });
117
-
118
- it('updateFeedItem should delegate to feedService.updateFeedItem', async () => {
119
- const result = await protocol.updateFeedItem({
120
- object: 'account',
121
- recordId: 'rec_123',
122
- feedId: 'feed_1',
123
- body: 'Updated',
124
- });
125
-
126
- expect(result.success).toBe(true);
127
- expect(result.data.body).toBe('Updated comment');
128
- expect(feedService.updateFeedItem).toHaveBeenCalledWith('feed_1', expect.objectContaining({ body: 'Updated' }));
129
- });
130
-
131
- it('deleteFeedItem should delegate to feedService.deleteFeedItem', async () => {
132
- const result = await protocol.deleteFeedItem({
133
- object: 'account',
134
- recordId: 'rec_123',
135
- feedId: 'feed_1',
136
- });
137
-
138
- expect(result.success).toBe(true);
139
- expect(result.data.feedId).toBe('feed_1');
140
- expect(feedService.deleteFeedItem).toHaveBeenCalledWith('feed_1');
141
- });
142
-
143
- // ==========================================
144
- // Reactions
145
- // ==========================================
146
-
147
- it('addReaction should delegate to feedService.addReaction', async () => {
148
- const result = await protocol.addReaction({
149
- object: 'account',
150
- recordId: 'rec_123',
151
- feedId: 'feed_1',
152
- emoji: '👍',
153
- });
154
-
155
- expect(result.success).toBe(true);
156
- expect(result.data.reactions).toHaveLength(1);
157
- expect(feedService.addReaction).toHaveBeenCalledWith('feed_1', '👍', 'current_user');
158
- });
159
-
160
- it('removeReaction should delegate to feedService.removeReaction', async () => {
161
- const result = await protocol.removeReaction({
162
- object: 'account',
163
- recordId: 'rec_123',
164
- feedId: 'feed_1',
165
- emoji: '👍',
166
- });
167
-
168
- expect(result.success).toBe(true);
169
- expect(result.data.reactions).toHaveLength(0);
170
- expect(feedService.removeReaction).toHaveBeenCalledWith('feed_1', '👍', 'current_user');
171
- });
172
-
173
- // ==========================================
174
- // Pin / Star
175
- // ==========================================
176
-
177
- it('pinFeedItem should verify item exists and return pinned status', async () => {
178
- const result = await protocol.pinFeedItem({
179
- object: 'account',
180
- recordId: 'rec_123',
181
- feedId: 'feed_1',
182
- });
183
-
184
- expect(result.success).toBe(true);
185
- expect(result.data.feedId).toBe('feed_1');
186
- expect(result.data.pinned).toBe(true);
187
- expect(result.data.pinnedAt).toBeDefined();
188
- });
189
-
190
- it('unpinFeedItem should verify item exists and return unpinned status', async () => {
191
- const result = await protocol.unpinFeedItem({
192
- object: 'account',
193
- recordId: 'rec_123',
194
- feedId: 'feed_1',
195
- });
196
-
197
- expect(result.success).toBe(true);
198
- expect(result.data.feedId).toBe('feed_1');
199
- expect(result.data.pinned).toBe(false);
200
- });
201
-
202
- it('starFeedItem should verify item exists and return starred status', async () => {
203
- const result = await protocol.starFeedItem({
204
- object: 'account',
205
- recordId: 'rec_123',
206
- feedId: 'feed_1',
207
- });
208
-
209
- expect(result.success).toBe(true);
210
- expect(result.data.feedId).toBe('feed_1');
211
- expect(result.data.starred).toBe(true);
212
- expect(result.data.starredAt).toBeDefined();
213
- });
214
-
215
- it('unstarFeedItem should verify item exists and return unstarred status', async () => {
216
- const result = await protocol.unstarFeedItem({
217
- object: 'account',
218
- recordId: 'rec_123',
219
- feedId: 'feed_1',
220
- });
221
-
222
- expect(result.success).toBe(true);
223
- expect(result.data.feedId).toBe('feed_1');
224
- expect(result.data.starred).toBe(false);
225
- });
226
-
227
- // ==========================================
228
- // Search & Changelog
229
- // ==========================================
230
-
231
- it('searchFeed should filter items by query text', async () => {
232
- const result = await protocol.searchFeed({
233
- object: 'account',
234
- recordId: 'rec_123',
235
- query: 'hello',
236
- });
237
-
238
- expect(result.success).toBe(true);
239
- expect(result.data.items).toHaveLength(1);
240
- expect(result.data.hasMore).toBe(false);
241
- });
242
-
243
- it('getChangelog should return field change entries', async () => {
244
- const result = await protocol.getChangelog({
245
- object: 'account',
246
- recordId: 'rec_123',
247
- });
248
-
249
- expect(result.success).toBe(true);
250
- expect(result.data.entries).toBeDefined();
251
- expect(feedService.listFeed).toHaveBeenCalledWith(
252
- expect.objectContaining({ filter: 'changes_only' })
253
- );
254
- });
255
-
256
- // ==========================================
257
- // Subscriptions
258
- // ==========================================
259
-
260
- it('feedSubscribe should delegate to feedService.subscribe', async () => {
261
- const result = await protocol.feedSubscribe({
262
- object: 'account',
263
- recordId: 'rec_123',
264
- events: ['all'],
265
- channels: ['in_app'],
266
- });
267
-
268
- expect(result.success).toBe(true);
269
- expect(result.data.object).toBe('account');
270
- expect(feedService.subscribe).toHaveBeenCalledWith(
271
- expect.objectContaining({ object: 'account', recordId: 'rec_123' })
272
- );
273
- });
274
-
275
- it('feedUnsubscribe should delegate to feedService.unsubscribe', async () => {
276
- const result = await protocol.feedUnsubscribe({
277
- object: 'account',
278
- recordId: 'rec_123',
279
- });
280
-
281
- expect(result.success).toBe(true);
282
- expect(result.data.unsubscribed).toBe(true);
283
- expect(feedService.unsubscribe).toHaveBeenCalledWith('account', 'rec_123', 'current_user');
284
- });
285
-
286
- // ==========================================
287
- // Error handling
288
- // ==========================================
289
-
290
- it('should throw when feed service is not available', async () => {
291
- const protocolNoFeed = new ObjectStackProtocolImplementation(engine);
292
-
293
- await expect(protocolNoFeed.listFeed({ object: 'a', recordId: 'b' }))
294
- .rejects.toThrow('Feed service not available');
295
- });
296
-
297
- it('pinFeedItem should throw when feed item not found', async () => {
298
- (feedService.getFeedItem as any).mockResolvedValue(null);
299
-
300
- await expect(protocol.pinFeedItem({ object: 'a', recordId: 'b', feedId: 'nonexistent' }))
301
- .rejects.toThrow('Feed item nonexistent not found');
302
- });
303
- });