gufi-cli 0.1.50 → 0.1.52
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.
- package/dist/commands/docs.js +1 -5
- package/dist/index.js +1 -0
- package/dist/lib/docs-resolver.d.ts +8 -0
- package/dist/lib/docs-resolver.js +27 -0
- package/dist/mcp.d.ts +3 -1
- package/dist/mcp.js +232 -34
- package/docs/dev-guide/1-01-architecture.md +358 -0
- package/docs/dev-guide/1-02-multi-tenant.md +415 -0
- package/docs/dev-guide/1-03-column-types.md +594 -0
- package/docs/dev-guide/1-04-json-config.md +442 -0
- package/docs/dev-guide/1-05-authentication.md +427 -0
- package/docs/dev-guide/2-01-api-reference.md +564 -0
- package/docs/dev-guide/2-02-automations.md +508 -0
- package/docs/dev-guide/2-03-gufi-cli.md +568 -0
- package/docs/dev-guide/2-04-realtime.md +401 -0
- package/docs/dev-guide/2-05-permissions.md +497 -0
- package/docs/dev-guide/2-06-integrations-overview.md +104 -0
- package/docs/dev-guide/2-07-stripe.md +173 -0
- package/docs/dev-guide/2-08-nayax.md +297 -0
- package/docs/dev-guide/2-09-ourvend.md +226 -0
- package/docs/dev-guide/2-10-tns.md +177 -0
- package/docs/dev-guide/2-11-custom-http.md +268 -0
- package/docs/dev-guide/3-01-custom-views.md +555 -0
- package/docs/dev-guide/3-02-webhooks-api.md +446 -0
- package/docs/mcp/00-overview.md +329 -0
- package/docs/mcp/01-architecture.md +220 -0
- package/docs/mcp/02-modules.md +285 -0
- package/docs/mcp/03-fields.md +357 -0
- package/docs/mcp/04-views.md +613 -0
- package/docs/mcp/05-automations.md +461 -0
- package/docs/mcp/06-api.md +480 -0
- package/docs/mcp/07-packages.md +246 -0
- package/docs/mcp/08-common-errors.md +284 -0
- package/docs/mcp/09-examples.md +453 -0
- package/docs/mcp/README.md +71 -0
- package/docs/mcp/tool-descriptions.json +49 -0
- package/package.json +3 -2
|
@@ -0,0 +1,401 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: realtime
|
|
3
|
+
title: "Real-Time & WebSocket"
|
|
4
|
+
description: "Live updates with PostgreSQL LISTEN/NOTIFY"
|
|
5
|
+
icon: Wifi
|
|
6
|
+
category: dev
|
|
7
|
+
part: 2
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# Real-Time & WebSocket
|
|
11
|
+
|
|
12
|
+
Live updates with PostgreSQL LISTEN/NOTIFY
|
|
13
|
+
|
|
14
|
+
## Architecture
|
|
15
|
+
|
|
16
|
+
Gufi uses PostgreSQL's LISTEN/NOTIFY for real-time updates, providing reliable, database-level event propagation.
|
|
17
|
+
|
|
18
|
+
```
|
|
19
|
+
┌──────────────────────────────────────────────────────────────┐
|
|
20
|
+
│ Real-Time System │
|
|
21
|
+
├──────────────────────────────────────────────────────────────┤
|
|
22
|
+
│ │
|
|
23
|
+
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
|
|
24
|
+
│ │ Database │ │ WebSocket │ │ Clients │ │
|
|
25
|
+
│ │ Trigger │────▶│ Server │────▶│ (React) │ │
|
|
26
|
+
│ │ + NOTIFY │ │ :4000 │ │ │ │
|
|
27
|
+
│ └──────────────┘ └──────────────┘ └──────────────┘ │
|
|
28
|
+
│ │
|
|
29
|
+
│ INSERT/UPDATE/DELETE │
|
|
30
|
+
│ │ │
|
|
31
|
+
│ ▼ │
|
|
32
|
+
│ pg_notify('company_146_m360_t4589', '{"type":"INSERT"...}') │
|
|
33
|
+
│ │
|
|
34
|
+
└──────────────────────────────────────────────────────────────┘
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## How It Works
|
|
38
|
+
|
|
39
|
+
### Database Triggers
|
|
40
|
+
|
|
41
|
+
When data changes, triggers fire NOTIFY:
|
|
42
|
+
|
|
43
|
+
```sql
|
|
44
|
+
-- Trigger function (created automatically)
|
|
45
|
+
CREATE FUNCTION notify_table_change() RETURNS TRIGGER AS $$
|
|
46
|
+
BEGIN
|
|
47
|
+
PERFORM pg_notify(
|
|
48
|
+
'company_' || current_setting('app.company_id') || '_' || TG_TABLE_NAME,
|
|
49
|
+
json_build_object(
|
|
50
|
+
'type', TG_OP,
|
|
51
|
+
'table', TG_TABLE_NAME,
|
|
52
|
+
'id', COALESCE(NEW.id, OLD.id),
|
|
53
|
+
'data', CASE TG_OP
|
|
54
|
+
WHEN 'DELETE' THEN row_to_json(OLD)
|
|
55
|
+
ELSE row_to_json(NEW)
|
|
56
|
+
END
|
|
57
|
+
)::text
|
|
58
|
+
);
|
|
59
|
+
RETURN COALESCE(NEW, OLD);
|
|
60
|
+
END;
|
|
61
|
+
$$ LANGUAGE plpgsql;
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### WebSocket Server
|
|
65
|
+
|
|
66
|
+
Listens to PostgreSQL channels and broadcasts:
|
|
67
|
+
|
|
68
|
+
```javascript
|
|
69
|
+
// Simplified WebSocket server logic
|
|
70
|
+
const pg = require('pg');
|
|
71
|
+
const WebSocket = require('ws');
|
|
72
|
+
|
|
73
|
+
// Listen to all company channels
|
|
74
|
+
const listenClient = new pg.Client();
|
|
75
|
+
await listenClient.connect();
|
|
76
|
+
|
|
77
|
+
// Subscribe to channels dynamically
|
|
78
|
+
async function subscribeToCompany(companyId, tableId) {
|
|
79
|
+
const channel = `company_${companyId}_${tableId}`;
|
|
80
|
+
await listenClient.query(`LISTEN ${channel}`);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Handle notifications
|
|
84
|
+
listenClient.on('notification', (msg) => {
|
|
85
|
+
const data = JSON.parse(msg.payload);
|
|
86
|
+
const [, companyId, tableId] = msg.channel.split('_');
|
|
87
|
+
|
|
88
|
+
// Broadcast to subscribed clients
|
|
89
|
+
broadcast(companyId, tableId, data);
|
|
90
|
+
});
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### Client Connection
|
|
94
|
+
|
|
95
|
+
```typescript
|
|
96
|
+
// React hook for WebSocket
|
|
97
|
+
import { useWebSocket } from '@/contexts/WebSocketContext';
|
|
98
|
+
|
|
99
|
+
function MyComponent() {
|
|
100
|
+
const { subscribe, isConnected } = useWebSocket();
|
|
101
|
+
|
|
102
|
+
useEffect(() => {
|
|
103
|
+
const unsubscribe = subscribe('m360_t4589', (event) => {
|
|
104
|
+
if (event.type === 'INSERT') {
|
|
105
|
+
// New record created
|
|
106
|
+
refetch();
|
|
107
|
+
} else if (event.type === 'UPDATE') {
|
|
108
|
+
// Record updated
|
|
109
|
+
updateLocalCache(event.id, event.data);
|
|
110
|
+
} else if (event.type === 'DELETE') {
|
|
111
|
+
// Record deleted
|
|
112
|
+
removeFromCache(event.id);
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
return () => unsubscribe();
|
|
117
|
+
}, []);
|
|
118
|
+
}
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
## Using Real-Time in Views
|
|
122
|
+
|
|
123
|
+
### Basic Subscription
|
|
124
|
+
|
|
125
|
+
```typescript
|
|
126
|
+
export default function MyView({ gufi }: Props) {
|
|
127
|
+
const { websocket, context } = gufi;
|
|
128
|
+
const [records, setRecords] = useState([]);
|
|
129
|
+
|
|
130
|
+
useEffect(() => {
|
|
131
|
+
const tableId = context.viewSpec?.ordersTable;
|
|
132
|
+
if (!tableId) return;
|
|
133
|
+
|
|
134
|
+
const unsubscribe = websocket.subscribeToTable(tableId, (event) => {
|
|
135
|
+
switch (event.type) {
|
|
136
|
+
case 'INSERT':
|
|
137
|
+
setRecords(prev => [...prev, event.data]);
|
|
138
|
+
break;
|
|
139
|
+
case 'UPDATE':
|
|
140
|
+
setRecords(prev =>
|
|
141
|
+
prev.map(r => r.id === event.id ? event.data : r)
|
|
142
|
+
);
|
|
143
|
+
break;
|
|
144
|
+
case 'DELETE':
|
|
145
|
+
setRecords(prev =>
|
|
146
|
+
prev.filter(r => r.id !== event.id)
|
|
147
|
+
);
|
|
148
|
+
break;
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
return () => unsubscribe();
|
|
153
|
+
}, [context.viewSpec?.ordersTable]);
|
|
154
|
+
|
|
155
|
+
return <RecordsList records={records} />;
|
|
156
|
+
}
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
### With SDK Hook
|
|
160
|
+
|
|
161
|
+
```typescript
|
|
162
|
+
const { data, loading, refetch } = useViewData({
|
|
163
|
+
gufi,
|
|
164
|
+
tableKey: 'ordersTable',
|
|
165
|
+
realtime: true // Enable auto-refresh on changes
|
|
166
|
+
});
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
## Event Types
|
|
170
|
+
|
|
171
|
+
### INSERT
|
|
172
|
+
|
|
173
|
+
```json
|
|
174
|
+
{
|
|
175
|
+
"type": "INSERT",
|
|
176
|
+
"table": "m360_t4589",
|
|
177
|
+
"id": 123,
|
|
178
|
+
"data": {
|
|
179
|
+
"id": 123,
|
|
180
|
+
"name": "New Product",
|
|
181
|
+
"price": 99.99,
|
|
182
|
+
"created_at": "2024-01-15T10:30:00Z"
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
### UPDATE
|
|
188
|
+
|
|
189
|
+
```json
|
|
190
|
+
{
|
|
191
|
+
"type": "UPDATE",
|
|
192
|
+
"table": "m360_t4589",
|
|
193
|
+
"id": 123,
|
|
194
|
+
"data": {
|
|
195
|
+
"id": 123,
|
|
196
|
+
"name": "Updated Product",
|
|
197
|
+
"price": 89.99,
|
|
198
|
+
"updated_at": "2024-01-15T11:00:00Z"
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
### DELETE
|
|
204
|
+
|
|
205
|
+
```json
|
|
206
|
+
{
|
|
207
|
+
"type": "DELETE",
|
|
208
|
+
"table": "m360_t4589",
|
|
209
|
+
"id": 123,
|
|
210
|
+
"data": {
|
|
211
|
+
"id": 123,
|
|
212
|
+
"name": "Deleted Product"
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
## Broadcasting Custom Events
|
|
218
|
+
|
|
219
|
+
### From Views
|
|
220
|
+
|
|
221
|
+
```typescript
|
|
222
|
+
// Broadcast to other users viewing the same view
|
|
223
|
+
websocket.broadcastToView('selection-changed', {
|
|
224
|
+
selectedIds: [1, 2, 3]
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
// Listen for broadcasts
|
|
228
|
+
websocket.onViewBroadcast('selection-changed', (data) => {
|
|
229
|
+
console.log('Another user selected:', data.selectedIds);
|
|
230
|
+
});
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
### From Backend
|
|
234
|
+
|
|
235
|
+
```javascript
|
|
236
|
+
// Send custom notification
|
|
237
|
+
const notifyClients = async (companyId, event) => {
|
|
238
|
+
await pool.query(
|
|
239
|
+
`SELECT pg_notify($1, $2)`,
|
|
240
|
+
[`company_${companyId}_custom`, JSON.stringify(event)]
|
|
241
|
+
);
|
|
242
|
+
};
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
## Connection Management
|
|
246
|
+
|
|
247
|
+
### Connection Status
|
|
248
|
+
|
|
249
|
+
```typescript
|
|
250
|
+
const { isConnected, reconnect } = useWebSocket();
|
|
251
|
+
|
|
252
|
+
if (!isConnected) {
|
|
253
|
+
return (
|
|
254
|
+
<div className="text-yellow-600">
|
|
255
|
+
<AlertCircle /> Connection lost. Reconnecting...
|
|
256
|
+
<button onClick={reconnect}>Retry Now</button>
|
|
257
|
+
</div>
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
### Automatic Reconnection
|
|
263
|
+
|
|
264
|
+
WebSocket client handles:
|
|
265
|
+
|
|
266
|
+
- Automatic reconnection on disconnect
|
|
267
|
+
- Exponential backoff (1s, 2s, 4s, 8s...)
|
|
268
|
+
- Re-subscription to channels after reconnect
|
|
269
|
+
- Heartbeat every 30 seconds
|
|
270
|
+
|
|
271
|
+
### Manual Subscription Management
|
|
272
|
+
|
|
273
|
+
```typescript
|
|
274
|
+
// Subscribe to multiple tables
|
|
275
|
+
const subscriptions = [
|
|
276
|
+
websocket.subscribeToTable(ordersTable, handleOrders),
|
|
277
|
+
websocket.subscribeToTable(productsTable, handleProducts),
|
|
278
|
+
];
|
|
279
|
+
|
|
280
|
+
// Cleanup
|
|
281
|
+
return () => {
|
|
282
|
+
subscriptions.forEach(unsub => unsub());
|
|
283
|
+
};
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
## Performance Considerations
|
|
287
|
+
|
|
288
|
+
### Debouncing Updates
|
|
289
|
+
|
|
290
|
+
For high-frequency changes:
|
|
291
|
+
|
|
292
|
+
```typescript
|
|
293
|
+
const [debouncedData, setDebouncedData] = useState(data);
|
|
294
|
+
|
|
295
|
+
useEffect(() => {
|
|
296
|
+
const timer = setTimeout(() => {
|
|
297
|
+
setDebouncedData(data);
|
|
298
|
+
}, 100); // 100ms debounce
|
|
299
|
+
|
|
300
|
+
return () => clearTimeout(timer);
|
|
301
|
+
}, [data]);
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
### Selective Subscriptions
|
|
305
|
+
|
|
306
|
+
Only subscribe to what you need:
|
|
307
|
+
|
|
308
|
+
```typescript
|
|
309
|
+
// Good: Subscribe to specific table
|
|
310
|
+
websocket.subscribeToTable(ordersTable, callback);
|
|
311
|
+
|
|
312
|
+
// Avoid: Don't subscribe to everything
|
|
313
|
+
// websocket.subscribeToAll(callback);
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
### Batching Updates
|
|
317
|
+
|
|
318
|
+
```typescript
|
|
319
|
+
const pendingUpdates = useRef([]);
|
|
320
|
+
|
|
321
|
+
useEffect(() => {
|
|
322
|
+
const processBatch = setInterval(() => {
|
|
323
|
+
if (pendingUpdates.current.length > 0) {
|
|
324
|
+
setRecords(prev => [...prev, ...pendingUpdates.current]);
|
|
325
|
+
pendingUpdates.current = [];
|
|
326
|
+
}
|
|
327
|
+
}, 500);
|
|
328
|
+
|
|
329
|
+
return () => clearInterval(processBatch);
|
|
330
|
+
}, []);
|
|
331
|
+
|
|
332
|
+
// In websocket handler
|
|
333
|
+
const handleEvent = (event) => {
|
|
334
|
+
pendingUpdates.current.push(event.data);
|
|
335
|
+
};
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
## Scaling
|
|
339
|
+
|
|
340
|
+
### Redis Pub/Sub
|
|
341
|
+
|
|
342
|
+
For horizontal scaling (multiple WebSocket servers):
|
|
343
|
+
|
|
344
|
+
```
|
|
345
|
+
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
|
|
346
|
+
│ WS Server │ │ WS Server │ │ WS Server │
|
|
347
|
+
│ #1 │ │ #2 │ │ #3 │
|
|
348
|
+
└──────┬──────┘ └──────┬──────┘ └──────┬──────┘
|
|
349
|
+
│ │ │
|
|
350
|
+
└───────────────────┼───────────────────┘
|
|
351
|
+
│
|
|
352
|
+
┌──────▼──────┐
|
|
353
|
+
│ Redis │
|
|
354
|
+
│ Pub/Sub │
|
|
355
|
+
└──────┬──────┘
|
|
356
|
+
│
|
|
357
|
+
┌──────▼──────┐
|
|
358
|
+
│ PostgreSQL │
|
|
359
|
+
│ NOTIFY │
|
|
360
|
+
└─────────────┘
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
### Configuration
|
|
364
|
+
|
|
365
|
+
```javascript
|
|
366
|
+
// WebSocket server with Redis
|
|
367
|
+
const Redis = require('ioredis');
|
|
368
|
+
const redis = new Redis(process.env.REDIS_URL);
|
|
369
|
+
|
|
370
|
+
// Publish to all servers
|
|
371
|
+
redis.publish('gufi:events', JSON.stringify(event));
|
|
372
|
+
|
|
373
|
+
// Subscribe on each server
|
|
374
|
+
redis.subscribe('gufi:events');
|
|
375
|
+
redis.on('message', (channel, message) => {
|
|
376
|
+
broadcastToLocalClients(JSON.parse(message));
|
|
377
|
+
});
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
## Troubleshooting
|
|
381
|
+
|
|
382
|
+
### No Updates Received
|
|
383
|
+
|
|
384
|
+
1. Check WebSocket connection status
|
|
385
|
+
2. Verify table triggers exist
|
|
386
|
+
3. Check channel name matches
|
|
387
|
+
4. Look for errors in browser console
|
|
388
|
+
|
|
389
|
+
### Delayed Updates
|
|
390
|
+
|
|
391
|
+
1. Check PostgreSQL connection
|
|
392
|
+
2. Monitor WebSocket server logs
|
|
393
|
+
3. Verify network latency
|
|
394
|
+
4. Check for batching/debouncing
|
|
395
|
+
|
|
396
|
+
### Memory Issues
|
|
397
|
+
|
|
398
|
+
1. Ensure subscriptions are cleaned up
|
|
399
|
+
2. Check for memory leaks in callbacks
|
|
400
|
+
3. Monitor connection count
|
|
401
|
+
4. Use pagination for large datasets
|