@squidcloud/cli 1.0.422 → 1.0.424
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/index.js
CHANGED
|
@@ -11423,7 +11423,7 @@ Object.defineProperty(exports, "__esModule", ({ value: true }));
|
|
|
11423
11423
|
exports.CONNECTOR_METADATA_JSON_FILE = exports.CONNECTOR_IDS = void 0;
|
|
11424
11424
|
/**
|
|
11425
11425
|
* List of all connector package names.
|
|
11426
|
-
* To prevent automatic discovery && latest version sync in 'prod' add
|
|
11426
|
+
* To prevent automatic discovery && latest version sync in 'prod' add your connector package name to
|
|
11427
11427
|
* 'CONNECTOR_PACKAGES_TO_EXCLUDE_FROM_AUTO_SYNC' in ConnectorsService (console-backend).
|
|
11428
11428
|
*/
|
|
11429
11429
|
exports.CONNECTOR_IDS = [
|
|
@@ -11431,6 +11431,7 @@ exports.CONNECTOR_IDS = [
|
|
|
11431
11431
|
'cotomi',
|
|
11432
11432
|
'essentials',
|
|
11433
11433
|
'google_calendar',
|
|
11434
|
+
'google_drive',
|
|
11434
11435
|
'hubspot',
|
|
11435
11436
|
'jira',
|
|
11436
11437
|
'mail',
|
|
@@ -30623,7 +30624,7 @@ function exitWithError(...messages) {
|
|
|
30623
30624
|
/***/ ((module) => {
|
|
30624
30625
|
|
|
30625
30626
|
"use strict";
|
|
30626
|
-
module.exports = /*#__PURE__*/JSON.parse('{"name":"@squidcloud/cli","version":"1.0.
|
|
30627
|
+
module.exports = /*#__PURE__*/JSON.parse('{"name":"@squidcloud/cli","version":"1.0.424","description":"The Squid CLI","main":"dist/index.js","scripts":{"start":"node dist/index.js","start-ts":"ts-node -r tsconfig-paths/register src/index.ts","prebuild":"rimraf dist","build":"webpack --mode=production","build:dev":"webpack --mode=development","lint":"eslint","link":"npm run build && chmod 755 dist/index.js && npm link","watch":"webpack --watch","deploy":"npm run build && npm pack --silent | xargs -I {} mv {} package.tgz && npm install -g package.tgz && rm -rf package.tgz","publish:public":"npm run build && npm publish --access public"},"files":["dist/**/*"],"bin":{"squid":"dist/index.js"},"keywords":[],"author":"","license":"ISC","engines":{"node":">=18.0.0"},"dependencies":{"@squidcloud/local-backend":"^1.0.424","adm-zip":"^0.5.16","copy-webpack-plugin":"^12.0.2","decompress":"^4.2.1","nodemon":"^3.1.9","terser-webpack-plugin":"^5.3.10","ts-loader":"^9.5.1","ts-node":"^10.9.2","tsconfig-paths":"^4.2.0","tsconfig-paths-webpack-plugin":"^4.1.0","webpack":"^5.101.3","zip-webpack-plugin":"^4.0.1"},"devDependencies":{"@types/adm-zip":"^0.5.7","@types/decompress":"^4.2.7","@types/node":"^20.19.9","terminal-link":"^3.0.0"}}');
|
|
30627
30628
|
|
|
30628
30629
|
/***/ }),
|
|
30629
30630
|
|
|
@@ -0,0 +1,1244 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: Squid React Development
|
|
3
|
+
description: Provides comprehensive, code-verified knowledge about developing React applications with Squid using @squidcloud/react. Use this skill proactively before writing, editing, or creating any React code that uses Squid's React SDK hooks to ensure compliance with Squid's React APIs and best practices.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Squid React Development Skill (Verified)
|
|
7
|
+
|
|
8
|
+
This skill provides comprehensive, code-verified knowledge about developing React applications with Squid using the `@squidcloud/react` SDK.
|
|
9
|
+
|
|
10
|
+
## Overview
|
|
11
|
+
|
|
12
|
+
`@squidcloud/react` is a React wrapper for the Squid Client SDK (`@squidcloud/client`) that provides:
|
|
13
|
+
- React Context provider for Squid instance management
|
|
14
|
+
- Custom hooks for state management with Squid backend services
|
|
15
|
+
- Support for databases, APIs, AI agents, and real-time data
|
|
16
|
+
- Full TypeScript support with strict type safety
|
|
17
|
+
|
|
18
|
+
**Key Point:** `@squidcloud/react` provides partial hooks for common operations. For any functionality not covered by a dedicated hook, use `useSquid()` to access the full Squid client SDK.
|
|
19
|
+
|
|
20
|
+
## Installation
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
npm install @squidcloud/react @squidcloud/client rxjs
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
**Peer Dependencies:**
|
|
27
|
+
- `@squidcloud/client` (^1.0.415+)
|
|
28
|
+
- `react` (16.11+, 17+, 18+, 19+)
|
|
29
|
+
- `rxjs` (>=7.5.7 <8.0.0)
|
|
30
|
+
|
|
31
|
+
## Setup: SquidContextProvider
|
|
32
|
+
|
|
33
|
+
Wrap your application with `SquidContextProvider` to make the Squid instance available to all child components.
|
|
34
|
+
|
|
35
|
+
```typescript
|
|
36
|
+
import { SquidContextProvider } from '@squidcloud/react';
|
|
37
|
+
import { createRoot } from 'react-dom/client';
|
|
38
|
+
import App from './App';
|
|
39
|
+
|
|
40
|
+
createRoot(document.getElementById('root')!).render(
|
|
41
|
+
<SquidContextProvider
|
|
42
|
+
options={{
|
|
43
|
+
appId: import.meta.env.VITE_SQUID_APP_ID,
|
|
44
|
+
region: import.meta.env.VITE_SQUID_REGION,
|
|
45
|
+
environmentId: import.meta.env.VITE_SQUID_ENVIRONMENT_ID,
|
|
46
|
+
squidDeveloperId: import.meta.env.VITE_SQUID_DEVELOPER_ID,
|
|
47
|
+
// Optional: API key for AI queries and admin operations
|
|
48
|
+
apiKey: import.meta.env.VITE_SQUID_API_KEY,
|
|
49
|
+
}}
|
|
50
|
+
>
|
|
51
|
+
<App />
|
|
52
|
+
</SquidContextProvider>
|
|
53
|
+
);
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### SquidContextProvider Props
|
|
57
|
+
|
|
58
|
+
```typescript
|
|
59
|
+
interface SquidContextProps {
|
|
60
|
+
children: React.ReactNode;
|
|
61
|
+
options: SquidOptions; // Same options as Squid client SDK
|
|
62
|
+
}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
The `options` prop accepts all standard Squid client options including:
|
|
66
|
+
- `appId` - Your Squid application ID
|
|
67
|
+
- `region` - Squid region (e.g., 'us-east-1.aws')
|
|
68
|
+
- `environmentId` - Environment ID (e.g., 'dev', 'prod')
|
|
69
|
+
- `squidDeveloperId` - Your Squid developer ID
|
|
70
|
+
- `apiKey` - Optional API key for elevated permissions
|
|
71
|
+
- `authProvider` - Optional authentication provider
|
|
72
|
+
|
|
73
|
+
## Core Hooks
|
|
74
|
+
|
|
75
|
+
### useSquid
|
|
76
|
+
|
|
77
|
+
Access the Squid client instance directly. Use this for any operations not covered by other hooks.
|
|
78
|
+
|
|
79
|
+
```typescript
|
|
80
|
+
import { useSquid } from '@squidcloud/react';
|
|
81
|
+
|
|
82
|
+
function MyComponent() {
|
|
83
|
+
const squid = useSquid();
|
|
84
|
+
|
|
85
|
+
// Now you can use any Squid client SDK method
|
|
86
|
+
const handleClick = async () => {
|
|
87
|
+
// Execute backend function
|
|
88
|
+
await squid.executeFunction('myFunction', { param: 'value' });
|
|
89
|
+
|
|
90
|
+
// Access AI
|
|
91
|
+
const agent = squid.ai().agent('my-agent');
|
|
92
|
+
|
|
93
|
+
// Access storage
|
|
94
|
+
const storage = squid.storage();
|
|
95
|
+
|
|
96
|
+
// Access queues
|
|
97
|
+
const queue = squid.queue('my-queue');
|
|
98
|
+
|
|
99
|
+
// Any other Squid client SDK operation
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
return <button onClick={handleClick}>Do Something</button>;
|
|
103
|
+
}
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
**Important:** `useSquid()` must be used within a `SquidContextProvider`. It throws an error if used outside.
|
|
107
|
+
|
|
108
|
+
### useCollection
|
|
109
|
+
|
|
110
|
+
Get a reference to a Squid collection for querying and mutations.
|
|
111
|
+
|
|
112
|
+
```typescript
|
|
113
|
+
import { useCollection } from '@squidcloud/react';
|
|
114
|
+
|
|
115
|
+
interface User {
|
|
116
|
+
id: string;
|
|
117
|
+
name: string;
|
|
118
|
+
email: string;
|
|
119
|
+
active: boolean;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function UserList() {
|
|
123
|
+
const usersCollection = useCollection<User>('users');
|
|
124
|
+
|
|
125
|
+
// Use collection for queries
|
|
126
|
+
const query = usersCollection.query().where('active', '==', true);
|
|
127
|
+
|
|
128
|
+
// Use collection for mutations
|
|
129
|
+
const addUser = async (user: User) => {
|
|
130
|
+
await usersCollection.doc({ id: user.id }).insert(user);
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
return <div>...</div>;
|
|
134
|
+
}
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
**Signature:**
|
|
138
|
+
```typescript
|
|
139
|
+
function useCollection<T extends DocumentData>(
|
|
140
|
+
collectionName: CollectionName,
|
|
141
|
+
integrationId?: IntegrationId,
|
|
142
|
+
): CollectionReference<T>
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
### useQuery
|
|
146
|
+
|
|
147
|
+
Subscribe to real-time query updates or fetch a single snapshot.
|
|
148
|
+
|
|
149
|
+
```typescript
|
|
150
|
+
import { useQuery, useCollection } from '@squidcloud/react';
|
|
151
|
+
|
|
152
|
+
interface Task {
|
|
153
|
+
id: string;
|
|
154
|
+
title: string;
|
|
155
|
+
completed: boolean;
|
|
156
|
+
userId: string;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function TaskList({ userId }: { userId: string }) {
|
|
160
|
+
const tasksCollection = useCollection<Task>('tasks');
|
|
161
|
+
const query = tasksCollection.query().where('userId', '==', userId);
|
|
162
|
+
|
|
163
|
+
const { loading, data, error } = useQuery(query, {
|
|
164
|
+
enabled: !!userId, // Only run when userId is set
|
|
165
|
+
subscribe: true, // Real-time updates (default)
|
|
166
|
+
initialData: [], // Initial data before first load
|
|
167
|
+
}, [userId]); // Re-subscribe when userId changes
|
|
168
|
+
|
|
169
|
+
if (loading) return <div>Loading...</div>;
|
|
170
|
+
if (error) return <div>Error: {error.message}</div>;
|
|
171
|
+
|
|
172
|
+
return (
|
|
173
|
+
<ul>
|
|
174
|
+
{data.map(task => (
|
|
175
|
+
<li key={task.id}>{task.title}</li>
|
|
176
|
+
))}
|
|
177
|
+
</ul>
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
**Signature:**
|
|
183
|
+
```typescript
|
|
184
|
+
function useQuery<T>(
|
|
185
|
+
query: SnapshotEmitter<T>,
|
|
186
|
+
options?: QueryOptions<T>,
|
|
187
|
+
deps?: ReadonlyArray<unknown>,
|
|
188
|
+
): QueryType<T>
|
|
189
|
+
|
|
190
|
+
interface QueryOptions<T> {
|
|
191
|
+
enabled?: boolean; // Default: true
|
|
192
|
+
subscribe?: boolean; // Default: true (continuous updates)
|
|
193
|
+
initialData?: Array<T>; // Default: []
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
interface QueryType<T> {
|
|
197
|
+
loading: boolean;
|
|
198
|
+
data: Array<T>;
|
|
199
|
+
error: any;
|
|
200
|
+
}
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
**Single Snapshot vs Real-time:**
|
|
204
|
+
```typescript
|
|
205
|
+
// Real-time updates (subscribe: true) - default
|
|
206
|
+
const { data } = useQuery(query, { subscribe: true });
|
|
207
|
+
|
|
208
|
+
// Single snapshot (subscribe: false) - fetches once
|
|
209
|
+
const { data } = useQuery(query, { subscribe: false });
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
### useDoc
|
|
213
|
+
|
|
214
|
+
Get a single document with real-time updates.
|
|
215
|
+
|
|
216
|
+
```typescript
|
|
217
|
+
import { useDoc, useCollection } from '@squidcloud/react';
|
|
218
|
+
|
|
219
|
+
interface User {
|
|
220
|
+
id: string;
|
|
221
|
+
name: string;
|
|
222
|
+
email: string;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function UserProfile({ userId }: { userId: string }) {
|
|
226
|
+
const usersCollection = useCollection<User>('users');
|
|
227
|
+
const docRef = usersCollection.doc({ id: userId });
|
|
228
|
+
|
|
229
|
+
const { loading, data, error } = useDoc(docRef, {
|
|
230
|
+
enabled: !!userId,
|
|
231
|
+
subscribe: true, // Default: true
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
if (loading) return <div>Loading...</div>;
|
|
235
|
+
if (error) return <div>Error: {error.message}</div>;
|
|
236
|
+
if (!data) return <div>User not found</div>;
|
|
237
|
+
|
|
238
|
+
return <div>{data.name}</div>;
|
|
239
|
+
}
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
**Signature:**
|
|
243
|
+
```typescript
|
|
244
|
+
function useDoc<T extends DocumentData>(
|
|
245
|
+
doc: DocumentReference<T>,
|
|
246
|
+
options?: DocOptions,
|
|
247
|
+
): DocType<T>
|
|
248
|
+
|
|
249
|
+
interface DocOptions {
|
|
250
|
+
enabled?: boolean; // Default: true
|
|
251
|
+
subscribe?: boolean; // Default: true
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
interface DocType<T> {
|
|
255
|
+
loading: boolean;
|
|
256
|
+
data: T | undefined;
|
|
257
|
+
error: any;
|
|
258
|
+
}
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
### useDocs
|
|
262
|
+
|
|
263
|
+
Get multiple documents in parallel with real-time updates.
|
|
264
|
+
|
|
265
|
+
```typescript
|
|
266
|
+
import { useDocs, useCollection } from '@squidcloud/react';
|
|
267
|
+
|
|
268
|
+
interface User {
|
|
269
|
+
id: string;
|
|
270
|
+
name: string;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function UserAvatars({ userIds }: { userIds: Array<string> }) {
|
|
274
|
+
const usersCollection = useCollection<User>('users');
|
|
275
|
+
const docRefs = userIds.map(id => usersCollection.doc({ id }));
|
|
276
|
+
|
|
277
|
+
const { loading, data, error } = useDocs(docRefs, {
|
|
278
|
+
enabled: userIds.length > 0,
|
|
279
|
+
subscribe: true,
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
if (loading) return <div>Loading...</div>;
|
|
283
|
+
if (error) return <div>Error: {error.message}</div>;
|
|
284
|
+
|
|
285
|
+
return (
|
|
286
|
+
<div>
|
|
287
|
+
{data.map((user, i) => (
|
|
288
|
+
<span key={userIds[i]}>{user?.name ?? 'Unknown'}</span>
|
|
289
|
+
))}
|
|
290
|
+
</div>
|
|
291
|
+
);
|
|
292
|
+
}
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
**Signature:**
|
|
296
|
+
```typescript
|
|
297
|
+
function useDocs<T extends DocumentData>(
|
|
298
|
+
docs: Array<DocumentReference<T>>,
|
|
299
|
+
options?: DocOptions,
|
|
300
|
+
): DocsType<T>
|
|
301
|
+
|
|
302
|
+
interface DocsType<T> {
|
|
303
|
+
loading: boolean;
|
|
304
|
+
data: Array<T | undefined>; // Matches order of input refs
|
|
305
|
+
error: any;
|
|
306
|
+
}
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
### usePagination
|
|
310
|
+
|
|
311
|
+
Paginate query results with navigation controls.
|
|
312
|
+
|
|
313
|
+
```typescript
|
|
314
|
+
import { usePagination, useCollection } from '@squidcloud/react';
|
|
315
|
+
|
|
316
|
+
interface Post {
|
|
317
|
+
id: string;
|
|
318
|
+
title: string;
|
|
319
|
+
createdAt: number;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function PostList() {
|
|
323
|
+
const postsCollection = useCollection<Post>('posts');
|
|
324
|
+
const query = postsCollection.query().sortBy('createdAt', false);
|
|
325
|
+
|
|
326
|
+
const { loading, data, hasNext, hasPrev, next, prev } = usePagination(
|
|
327
|
+
query,
|
|
328
|
+
{
|
|
329
|
+
pageSize: 10,
|
|
330
|
+
enabled: true,
|
|
331
|
+
},
|
|
332
|
+
[], // Dependencies - pagination resets when these change
|
|
333
|
+
);
|
|
334
|
+
|
|
335
|
+
return (
|
|
336
|
+
<div>
|
|
337
|
+
{loading && <div>Loading...</div>}
|
|
338
|
+
|
|
339
|
+
<ul>
|
|
340
|
+
{data.map(post => (
|
|
341
|
+
<li key={post.id}>{post.title}</li>
|
|
342
|
+
))}
|
|
343
|
+
</ul>
|
|
344
|
+
|
|
345
|
+
<div>
|
|
346
|
+
<button onClick={prev} disabled={!hasPrev}>Previous</button>
|
|
347
|
+
<button onClick={next} disabled={!hasNext}>Next</button>
|
|
348
|
+
</div>
|
|
349
|
+
</div>
|
|
350
|
+
);
|
|
351
|
+
}
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
**Signature:**
|
|
355
|
+
```typescript
|
|
356
|
+
function usePagination<T>(
|
|
357
|
+
query: Pick<SnapshotEmitter<T>, 'paginate'>,
|
|
358
|
+
options: PaginationOptions,
|
|
359
|
+
deps?: ReadonlyArray<unknown>,
|
|
360
|
+
): PaginationType<T>
|
|
361
|
+
|
|
362
|
+
interface PaginationOptions {
|
|
363
|
+
pageSize: number; // Required: items per page
|
|
364
|
+
enabled?: boolean; // Default: true
|
|
365
|
+
// Other Squid pagination options...
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
interface PaginationType<T> {
|
|
369
|
+
loading: boolean;
|
|
370
|
+
data: Array<T>;
|
|
371
|
+
hasNext: boolean;
|
|
372
|
+
hasPrev: boolean;
|
|
373
|
+
next: () => void;
|
|
374
|
+
prev: () => void;
|
|
375
|
+
}
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
## AI Hooks
|
|
379
|
+
|
|
380
|
+
### useAiAgent
|
|
381
|
+
|
|
382
|
+
Main hook for interacting with AI agents.
|
|
383
|
+
|
|
384
|
+
```typescript
|
|
385
|
+
import { useAiAgent } from '@squidcloud/react';
|
|
386
|
+
|
|
387
|
+
function ChatBot() {
|
|
388
|
+
const {
|
|
389
|
+
chat,
|
|
390
|
+
transcribeAndChat,
|
|
391
|
+
chatWithVoiceResponse,
|
|
392
|
+
transcribeAndChatWithVoiceResponse,
|
|
393
|
+
history,
|
|
394
|
+
statusUpdates,
|
|
395
|
+
data,
|
|
396
|
+
loading,
|
|
397
|
+
error,
|
|
398
|
+
complete,
|
|
399
|
+
} = useAiAgent('support-agent', {
|
|
400
|
+
// Optional default chat options
|
|
401
|
+
smoothTyping: true,
|
|
402
|
+
}, {
|
|
403
|
+
// Optional client options
|
|
404
|
+
apiKey: import.meta.env.VITE_SQUID_AGENT_API_KEY,
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
const [input, setInput] = useState('');
|
|
408
|
+
|
|
409
|
+
const handleSend = () => {
|
|
410
|
+
chat(input, {
|
|
411
|
+
// Per-message options (override defaults)
|
|
412
|
+
memoryOptions: { memoryId: 'session-123' },
|
|
413
|
+
});
|
|
414
|
+
setInput('');
|
|
415
|
+
};
|
|
416
|
+
|
|
417
|
+
return (
|
|
418
|
+
<div>
|
|
419
|
+
{/* Display chat history */}
|
|
420
|
+
<div>
|
|
421
|
+
{history.map((msg) => (
|
|
422
|
+
<div key={msg.id} className={msg.type}>
|
|
423
|
+
{msg.message}
|
|
424
|
+
</div>
|
|
425
|
+
))}
|
|
426
|
+
{loading && <div>AI is typing...</div>}
|
|
427
|
+
</div>
|
|
428
|
+
|
|
429
|
+
{/* Input */}
|
|
430
|
+
<input
|
|
431
|
+
value={input}
|
|
432
|
+
onChange={(e) => setInput(e.target.value)}
|
|
433
|
+
disabled={loading}
|
|
434
|
+
/>
|
|
435
|
+
<button onClick={handleSend} disabled={loading}>Send</button>
|
|
436
|
+
</div>
|
|
437
|
+
);
|
|
438
|
+
}
|
|
439
|
+
```
|
|
440
|
+
|
|
441
|
+
**Signature:**
|
|
442
|
+
```typescript
|
|
443
|
+
function useAiAgent(
|
|
444
|
+
agentId: AiAgentId,
|
|
445
|
+
chatOptions?: AiChatOptions,
|
|
446
|
+
clientOptions?: AiAgentClientOptions,
|
|
447
|
+
): AiHookResponse
|
|
448
|
+
|
|
449
|
+
interface AiHookResponse {
|
|
450
|
+
// Methods
|
|
451
|
+
chat: (prompt: string, options?: AiChatOptions, jobId?: JobId) => void;
|
|
452
|
+
transcribeAndChat: (file: File, options?: AiChatOptions, jobId?: JobId) => void;
|
|
453
|
+
chatWithVoiceResponse: (prompt: string, options?: Omit<AiChatOptions, 'smoothTyping'>, jobId?: JobId) => void;
|
|
454
|
+
transcribeAndChatWithVoiceResponse: (file: File, options?: Omit<AiChatOptions, 'smoothTyping'>, jobId?: JobId) => void;
|
|
455
|
+
|
|
456
|
+
// State
|
|
457
|
+
history: Array<ChatMessage>;
|
|
458
|
+
statusUpdates: Record<JobId, Array<AiStatusMessage>>;
|
|
459
|
+
data: string; // Latest response text
|
|
460
|
+
loading: boolean;
|
|
461
|
+
error: any;
|
|
462
|
+
complete: boolean;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
interface ChatMessage {
|
|
466
|
+
id: string;
|
|
467
|
+
type: 'ai' | 'user';
|
|
468
|
+
message: string;
|
|
469
|
+
jobId: JobId | undefined;
|
|
470
|
+
voiceFile?: File; // Only on AI messages with voice
|
|
471
|
+
}
|
|
472
|
+
```
|
|
473
|
+
|
|
474
|
+
**Chat with Memory:**
|
|
475
|
+
```typescript
|
|
476
|
+
const { chat, history } = useAiAgent('my-agent');
|
|
477
|
+
|
|
478
|
+
// Messages are persisted and loaded automatically
|
|
479
|
+
chat('Hello!', {
|
|
480
|
+
memoryOptions: {
|
|
481
|
+
memoryId: 'user-123-session', // Unique ID for conversation
|
|
482
|
+
},
|
|
483
|
+
});
|
|
484
|
+
```
|
|
485
|
+
|
|
486
|
+
**Voice Interactions:**
|
|
487
|
+
```typescript
|
|
488
|
+
const { transcribeAndChat, chatWithVoiceResponse } = useAiAgent('voice-agent');
|
|
489
|
+
|
|
490
|
+
// Transcribe audio and get text response
|
|
491
|
+
const handleAudioInput = (audioFile: File) => {
|
|
492
|
+
transcribeAndChat(audioFile);
|
|
493
|
+
};
|
|
494
|
+
|
|
495
|
+
// Send text and get voice response
|
|
496
|
+
const handleTextWithVoice = (text: string) => {
|
|
497
|
+
chatWithVoiceResponse(text, {
|
|
498
|
+
voiceOptions: {
|
|
499
|
+
voiceId: 'alloy',
|
|
500
|
+
model: 'tts-1',
|
|
501
|
+
},
|
|
502
|
+
});
|
|
503
|
+
};
|
|
504
|
+
```
|
|
505
|
+
|
|
506
|
+
### useAiChat
|
|
507
|
+
|
|
508
|
+
Simplified wrapper for `useAiAgent` when you only need basic chat functionality.
|
|
509
|
+
|
|
510
|
+
```typescript
|
|
511
|
+
import { useAiChat } from '@squidcloud/react';
|
|
512
|
+
|
|
513
|
+
function SimpleChat() {
|
|
514
|
+
const { chat, history, loading } = useAiChat('my-agent');
|
|
515
|
+
|
|
516
|
+
return (
|
|
517
|
+
<div>
|
|
518
|
+
{history.map(msg => (
|
|
519
|
+
<div key={msg.id}>{msg.type}: {msg.message}</div>
|
|
520
|
+
))}
|
|
521
|
+
<button onClick={() => chat('Hello!')}>Say Hello</button>
|
|
522
|
+
</div>
|
|
523
|
+
);
|
|
524
|
+
}
|
|
525
|
+
```
|
|
526
|
+
|
|
527
|
+
### useAiQuery
|
|
528
|
+
|
|
529
|
+
AI-powered database queries. Requires Squid API key.
|
|
530
|
+
|
|
531
|
+
```typescript
|
|
532
|
+
import { useAiQuery } from '@squidcloud/react';
|
|
533
|
+
|
|
534
|
+
function DataExplorer() {
|
|
535
|
+
const { chat, data, loading, error } = useAiQuery('my-database', {
|
|
536
|
+
// Optional AI query options
|
|
537
|
+
});
|
|
538
|
+
|
|
539
|
+
const [question, setQuestion] = useState('');
|
|
540
|
+
|
|
541
|
+
const handleAsk = () => {
|
|
542
|
+
chat(question); // e.g., "Show me all users who signed up last month"
|
|
543
|
+
};
|
|
544
|
+
|
|
545
|
+
return (
|
|
546
|
+
<div>
|
|
547
|
+
<input
|
|
548
|
+
value={question}
|
|
549
|
+
onChange={(e) => setQuestion(e.target.value)}
|
|
550
|
+
placeholder="Ask about your data..."
|
|
551
|
+
/>
|
|
552
|
+
<button onClick={handleAsk}>Ask</button>
|
|
553
|
+
|
|
554
|
+
{loading && <div>Thinking...</div>}
|
|
555
|
+
{data && <div dangerouslySetInnerHTML={{ __html: data }} />}
|
|
556
|
+
</div>
|
|
557
|
+
);
|
|
558
|
+
}
|
|
559
|
+
```
|
|
560
|
+
|
|
561
|
+
### useAiOnApi
|
|
562
|
+
|
|
563
|
+
AI-powered API querying.
|
|
564
|
+
|
|
565
|
+
```typescript
|
|
566
|
+
import { useAiOnApi } from '@squidcloud/react';
|
|
567
|
+
|
|
568
|
+
function ApiExplorer() {
|
|
569
|
+
const { chat, data, loading } = useAiOnApi(
|
|
570
|
+
'my-api-integration',
|
|
571
|
+
['GET /users', 'GET /orders'], // Optional: restrict endpoints
|
|
572
|
+
true, // Include explanation
|
|
573
|
+
);
|
|
574
|
+
|
|
575
|
+
return (
|
|
576
|
+
<div>
|
|
577
|
+
<button onClick={() => chat('Get all active users')}>
|
|
578
|
+
Query API
|
|
579
|
+
</button>
|
|
580
|
+
{loading && <div>Loading...</div>}
|
|
581
|
+
{data && <pre>{data}</pre>}
|
|
582
|
+
</div>
|
|
583
|
+
);
|
|
584
|
+
}
|
|
585
|
+
```
|
|
586
|
+
|
|
587
|
+
### useAskWithApi
|
|
588
|
+
|
|
589
|
+
Query a custom API endpoint with AI.
|
|
590
|
+
|
|
591
|
+
```typescript
|
|
592
|
+
import { useAskWithApi } from '@squidcloud/react';
|
|
593
|
+
|
|
594
|
+
function CustomApiChat() {
|
|
595
|
+
const { chat, data, loading, error } = useAskWithApi({
|
|
596
|
+
customApiUrl: 'https://my-api.com/ai/chat',
|
|
597
|
+
customApiHeaders: {
|
|
598
|
+
'X-Custom-Header': 'value',
|
|
599
|
+
},
|
|
600
|
+
agentId: 'optional-agent-id',
|
|
601
|
+
});
|
|
602
|
+
|
|
603
|
+
return (
|
|
604
|
+
<div>
|
|
605
|
+
<button onClick={() => chat('Hello custom API')}>Send</button>
|
|
606
|
+
{loading && <div>Loading...</div>}
|
|
607
|
+
{data && <div>{data}</div>}
|
|
608
|
+
</div>
|
|
609
|
+
);
|
|
610
|
+
}
|
|
611
|
+
```
|
|
612
|
+
|
|
613
|
+
## Utility Hooks
|
|
614
|
+
|
|
615
|
+
### useQueue
|
|
616
|
+
|
|
617
|
+
Subscribe to queue messages and produce new ones.
|
|
618
|
+
|
|
619
|
+
```typescript
|
|
620
|
+
import { useQueue, useSquid } from '@squidcloud/react';
|
|
621
|
+
|
|
622
|
+
interface Message {
|
|
623
|
+
id: string;
|
|
624
|
+
content: string;
|
|
625
|
+
timestamp: number;
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
function MessageQueue() {
|
|
629
|
+
const squid = useSquid();
|
|
630
|
+
const queue = squid.queue<Message>('notifications');
|
|
631
|
+
|
|
632
|
+
const { data, error, produce } = useQueue(queue, {
|
|
633
|
+
enabled: true,
|
|
634
|
+
});
|
|
635
|
+
|
|
636
|
+
const sendMessage = async () => {
|
|
637
|
+
await produce([{
|
|
638
|
+
id: crypto.randomUUID(),
|
|
639
|
+
content: 'Hello!',
|
|
640
|
+
timestamp: Date.now(),
|
|
641
|
+
}]);
|
|
642
|
+
};
|
|
643
|
+
|
|
644
|
+
return (
|
|
645
|
+
<div>
|
|
646
|
+
<div>Latest message: {data?.content}</div>
|
|
647
|
+
<button onClick={sendMessage}>Send Message</button>
|
|
648
|
+
</div>
|
|
649
|
+
);
|
|
650
|
+
}
|
|
651
|
+
```
|
|
652
|
+
|
|
653
|
+
**Signature:**
|
|
654
|
+
```typescript
|
|
655
|
+
function useQueue<T>(
|
|
656
|
+
queue: QueueManager<T>,
|
|
657
|
+
options?: QueueOptions,
|
|
658
|
+
deps?: ReadonlyArray<unknown>,
|
|
659
|
+
): QueueType<T>
|
|
660
|
+
|
|
661
|
+
interface QueueOptions {
|
|
662
|
+
enabled?: boolean; // Default: true
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
interface QueueType<T> {
|
|
666
|
+
data: T | null;
|
|
667
|
+
error: any;
|
|
668
|
+
produce: (messages: Array<T>) => Promise<void>;
|
|
669
|
+
}
|
|
670
|
+
```
|
|
671
|
+
|
|
672
|
+
### useObservable
|
|
673
|
+
|
|
674
|
+
Subscribe to any RxJS Observable. Base hook used by other hooks.
|
|
675
|
+
|
|
676
|
+
```typescript
|
|
677
|
+
import { useObservable, useSquid } from '@squidcloud/react';
|
|
678
|
+
|
|
679
|
+
function CustomSubscription() {
|
|
680
|
+
const squid = useSquid();
|
|
681
|
+
|
|
682
|
+
const { loading, data, error, complete } = useObservable(
|
|
683
|
+
() => squid.collection('users').query().snapshots(),
|
|
684
|
+
{
|
|
685
|
+
enabled: true,
|
|
686
|
+
initialData: [],
|
|
687
|
+
},
|
|
688
|
+
[], // Dependencies
|
|
689
|
+
);
|
|
690
|
+
|
|
691
|
+
return <div>{data.length} users</div>;
|
|
692
|
+
}
|
|
693
|
+
```
|
|
694
|
+
|
|
695
|
+
**Signature:**
|
|
696
|
+
```typescript
|
|
697
|
+
function useObservable<T>(
|
|
698
|
+
observable: () => Observable<T>,
|
|
699
|
+
options?: ObservableOptions<T>,
|
|
700
|
+
deps?: ReadonlyArray<unknown>,
|
|
701
|
+
): ObservableType<T>
|
|
702
|
+
|
|
703
|
+
interface ObservableOptions<T> {
|
|
704
|
+
enabled?: boolean; // Default: true
|
|
705
|
+
initialData?: T; // Default: null
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
interface ObservableType<T> {
|
|
709
|
+
loading: boolean;
|
|
710
|
+
data: T;
|
|
711
|
+
error: any;
|
|
712
|
+
complete: boolean;
|
|
713
|
+
}
|
|
714
|
+
```
|
|
715
|
+
|
|
716
|
+
### usePromise
|
|
717
|
+
|
|
718
|
+
Handle promise-based async operations.
|
|
719
|
+
|
|
720
|
+
```typescript
|
|
721
|
+
import { usePromise, useSquid } from '@squidcloud/react';
|
|
722
|
+
|
|
723
|
+
function AsyncData({ userId }: { userId: string }) {
|
|
724
|
+
const squid = useSquid();
|
|
725
|
+
|
|
726
|
+
const { loading, data, error } = usePromise(
|
|
727
|
+
() => squid.executeFunction('getUserDetails', { userId }),
|
|
728
|
+
{
|
|
729
|
+
enabled: !!userId,
|
|
730
|
+
initialData: null,
|
|
731
|
+
},
|
|
732
|
+
[userId],
|
|
733
|
+
);
|
|
734
|
+
|
|
735
|
+
if (loading) return <div>Loading...</div>;
|
|
736
|
+
if (error) return <div>Error: {error.message}</div>;
|
|
737
|
+
|
|
738
|
+
return <div>{data?.name}</div>;
|
|
739
|
+
}
|
|
740
|
+
```
|
|
741
|
+
|
|
742
|
+
**Signature:**
|
|
743
|
+
```typescript
|
|
744
|
+
function usePromise<T>(
|
|
745
|
+
promiseFn: () => Promise<T>,
|
|
746
|
+
options?: PromiseOptions<T>,
|
|
747
|
+
deps?: ReadonlyArray<unknown>,
|
|
748
|
+
): PromiseType<T>
|
|
749
|
+
|
|
750
|
+
interface PromiseOptions<T> {
|
|
751
|
+
enabled?: boolean; // Default: true
|
|
752
|
+
initialData?: T; // Default: null
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
interface PromiseType<T> {
|
|
756
|
+
loading: boolean;
|
|
757
|
+
data: T;
|
|
758
|
+
error: any;
|
|
759
|
+
}
|
|
760
|
+
```
|
|
761
|
+
|
|
762
|
+
## Server-Side Rendering (SSR)
|
|
763
|
+
|
|
764
|
+
### withServerQuery HOC
|
|
765
|
+
|
|
766
|
+
For Next.js and other SSR frameworks, use `withServerQuery` to fetch initial data on the server.
|
|
767
|
+
|
|
768
|
+
```typescript
|
|
769
|
+
import { withServerQuery, WithQueryProps } from '@squidcloud/react';
|
|
770
|
+
import { Squid } from '@squidcloud/client';
|
|
771
|
+
|
|
772
|
+
interface Post {
|
|
773
|
+
id: string;
|
|
774
|
+
title: string;
|
|
775
|
+
content: string;
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
// Component receives data prop from HOC
|
|
779
|
+
function PostListComponent({ data }: WithQueryProps<Post>) {
|
|
780
|
+
return (
|
|
781
|
+
<ul>
|
|
782
|
+
{data.map(post => (
|
|
783
|
+
<li key={post.id}>{post.title}</li>
|
|
784
|
+
))}
|
|
785
|
+
</ul>
|
|
786
|
+
);
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
// Create HOC with query
|
|
790
|
+
const squid = new Squid({ /* options */ });
|
|
791
|
+
const query = squid.collection<Post>('posts').query();
|
|
792
|
+
|
|
793
|
+
export const PostList = withServerQuery(
|
|
794
|
+
PostListComponent,
|
|
795
|
+
query,
|
|
796
|
+
{ subscribe: true }, // Continue real-time updates on client
|
|
797
|
+
);
|
|
798
|
+
```
|
|
799
|
+
|
|
800
|
+
**Signature:**
|
|
801
|
+
```typescript
|
|
802
|
+
function withServerQuery<C extends React.ComponentType<any>, T>(
|
|
803
|
+
Component: C,
|
|
804
|
+
query: SnapshotEmitter<T>,
|
|
805
|
+
options?: WithQueryOptions,
|
|
806
|
+
): React.FC<Omit<React.ComponentProps<C>, keyof WithQueryProps<T>>>
|
|
807
|
+
|
|
808
|
+
interface WithQueryProps<T> {
|
|
809
|
+
data: Array<T>; // Injected by HOC
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
interface WithQueryOptions {
|
|
813
|
+
subscribe?: boolean; // Default: true - continue updates on client
|
|
814
|
+
}
|
|
815
|
+
```
|
|
816
|
+
|
|
817
|
+
## Common Patterns
|
|
818
|
+
|
|
819
|
+
### Pattern 1: Conditional Queries
|
|
820
|
+
|
|
821
|
+
```typescript
|
|
822
|
+
function UserTasks({ userId }: { userId: string | null }) {
|
|
823
|
+
const tasksCollection = useCollection<Task>('tasks');
|
|
824
|
+
const query = userId
|
|
825
|
+
? tasksCollection.query().where('userId', '==', userId)
|
|
826
|
+
: tasksCollection.query().limit(0);
|
|
827
|
+
|
|
828
|
+
const { loading, data } = useQuery(query, {
|
|
829
|
+
enabled: !!userId, // Only runs when userId is set
|
|
830
|
+
}, [userId]);
|
|
831
|
+
|
|
832
|
+
return <div>{data.length} tasks</div>;
|
|
833
|
+
}
|
|
834
|
+
```
|
|
835
|
+
|
|
836
|
+
### Pattern 2: Dependent Queries
|
|
837
|
+
|
|
838
|
+
```typescript
|
|
839
|
+
function UserWithPosts({ userId }: { userId: string }) {
|
|
840
|
+
const usersCollection = useCollection<User>('users');
|
|
841
|
+
const postsCollection = useCollection<Post>('posts');
|
|
842
|
+
|
|
843
|
+
// First query: get user
|
|
844
|
+
const { data: user, loading: userLoading } = useDoc(
|
|
845
|
+
usersCollection.doc({ id: userId })
|
|
846
|
+
);
|
|
847
|
+
|
|
848
|
+
// Second query: depends on user
|
|
849
|
+
const postsQuery = postsCollection.query().where('authorId', '==', userId);
|
|
850
|
+
const { data: posts, loading: postsLoading } = useQuery(
|
|
851
|
+
postsQuery,
|
|
852
|
+
{ enabled: !!user }, // Only run when user is loaded
|
|
853
|
+
[userId],
|
|
854
|
+
);
|
|
855
|
+
|
|
856
|
+
if (userLoading || postsLoading) return <div>Loading...</div>;
|
|
857
|
+
|
|
858
|
+
return (
|
|
859
|
+
<div>
|
|
860
|
+
<h1>{user?.name}</h1>
|
|
861
|
+
<p>{posts.length} posts</p>
|
|
862
|
+
</div>
|
|
863
|
+
);
|
|
864
|
+
}
|
|
865
|
+
```
|
|
866
|
+
|
|
867
|
+
### Pattern 3: Mutations with useSquid
|
|
868
|
+
|
|
869
|
+
Hooks are for reading data. Use `useSquid()` for mutations:
|
|
870
|
+
|
|
871
|
+
```typescript
|
|
872
|
+
function TodoApp() {
|
|
873
|
+
const squid = useSquid();
|
|
874
|
+
const todosCollection = useCollection<Todo>('todos');
|
|
875
|
+
|
|
876
|
+
// Read with hook
|
|
877
|
+
const { data: todos } = useQuery(todosCollection.query());
|
|
878
|
+
|
|
879
|
+
// Mutations with squid client
|
|
880
|
+
const addTodo = async (title: string) => {
|
|
881
|
+
await todosCollection.doc({ id: crypto.randomUUID() }).insert({
|
|
882
|
+
id: crypto.randomUUID(),
|
|
883
|
+
title,
|
|
884
|
+
completed: false,
|
|
885
|
+
});
|
|
886
|
+
};
|
|
887
|
+
|
|
888
|
+
const toggleTodo = async (id: string, completed: boolean) => {
|
|
889
|
+
await todosCollection.doc({ id }).update({ completed });
|
|
890
|
+
};
|
|
891
|
+
|
|
892
|
+
const deleteTodo = async (id: string) => {
|
|
893
|
+
await todosCollection.doc({ id }).delete();
|
|
894
|
+
};
|
|
895
|
+
|
|
896
|
+
return (
|
|
897
|
+
<div>
|
|
898
|
+
<button onClick={() => addTodo('New Todo')}>Add</button>
|
|
899
|
+
{todos.map(todo => (
|
|
900
|
+
<div key={todo.id}>
|
|
901
|
+
<input
|
|
902
|
+
type="checkbox"
|
|
903
|
+
checked={todo.completed}
|
|
904
|
+
onChange={() => toggleTodo(todo.id, !todo.completed)}
|
|
905
|
+
/>
|
|
906
|
+
{todo.title}
|
|
907
|
+
<button onClick={() => deleteTodo(todo.id)}>Delete</button>
|
|
908
|
+
</div>
|
|
909
|
+
))}
|
|
910
|
+
</div>
|
|
911
|
+
);
|
|
912
|
+
}
|
|
913
|
+
```
|
|
914
|
+
|
|
915
|
+
### Pattern 4: Using Squid Client for Non-Hook Operations
|
|
916
|
+
|
|
917
|
+
For any Squid functionality without a dedicated hook, use `useSquid()`:
|
|
918
|
+
|
|
919
|
+
```typescript
|
|
920
|
+
function AdvancedFeatures() {
|
|
921
|
+
const squid = useSquid();
|
|
922
|
+
|
|
923
|
+
// Execute backend functions
|
|
924
|
+
const processPayment = async (amount: number) => {
|
|
925
|
+
return await squid.executeFunction('processPayment', { amount });
|
|
926
|
+
};
|
|
927
|
+
|
|
928
|
+
// File uploads
|
|
929
|
+
const uploadFile = async (file: File) => {
|
|
930
|
+
const storage = squid.storage();
|
|
931
|
+
return await storage.uploadFile(file, 'uploads/' + file.name);
|
|
932
|
+
};
|
|
933
|
+
|
|
934
|
+
// AI agent management
|
|
935
|
+
const createAgent = async () => {
|
|
936
|
+
const agent = squid.ai().agent('new-agent');
|
|
937
|
+
await agent.upsert({
|
|
938
|
+
description: 'My new agent',
|
|
939
|
+
options: { model: 'gpt-4o' },
|
|
940
|
+
});
|
|
941
|
+
};
|
|
942
|
+
|
|
943
|
+
// Transactions
|
|
944
|
+
const transferFunds = async (from: string, to: string, amount: number) => {
|
|
945
|
+
await squid.runInTransaction(async (txId) => {
|
|
946
|
+
const accounts = squid.collection('accounts');
|
|
947
|
+
|
|
948
|
+
const fromAccount = await accounts.doc({ id: from }).snapshot();
|
|
949
|
+
const toAccount = await accounts.doc({ id: to }).snapshot();
|
|
950
|
+
|
|
951
|
+
await accounts.doc({ id: from }).update(
|
|
952
|
+
{ balance: fromAccount!.balance - amount },
|
|
953
|
+
txId,
|
|
954
|
+
);
|
|
955
|
+
await accounts.doc({ id: to }).update(
|
|
956
|
+
{ balance: toAccount!.balance + amount },
|
|
957
|
+
txId,
|
|
958
|
+
);
|
|
959
|
+
});
|
|
960
|
+
};
|
|
961
|
+
|
|
962
|
+
return <div>...</div>;
|
|
963
|
+
}
|
|
964
|
+
```
|
|
965
|
+
|
|
966
|
+
### Pattern 5: Error Handling
|
|
967
|
+
|
|
968
|
+
```typescript
|
|
969
|
+
function SafeDataFetch() {
|
|
970
|
+
const { loading, data, error } = useQuery(/* ... */);
|
|
971
|
+
|
|
972
|
+
if (error) {
|
|
973
|
+
// Log error for debugging
|
|
974
|
+
console.error('Query failed:', error);
|
|
975
|
+
|
|
976
|
+
// Show user-friendly message
|
|
977
|
+
return (
|
|
978
|
+
<div className="error">
|
|
979
|
+
<p>Failed to load data. Please try again.</p>
|
|
980
|
+
<button onClick={() => window.location.reload()}>Retry</button>
|
|
981
|
+
</div>
|
|
982
|
+
);
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
if (loading) {
|
|
986
|
+
return <LoadingSpinner />;
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
return <DataDisplay data={data} />;
|
|
990
|
+
}
|
|
991
|
+
```
|
|
992
|
+
|
|
993
|
+
### Pattern 6: Optimistic Updates
|
|
994
|
+
|
|
995
|
+
```typescript
|
|
996
|
+
function OptimisticTodo() {
|
|
997
|
+
const squid = useSquid();
|
|
998
|
+
const todosCollection = useCollection<Todo>('todos');
|
|
999
|
+
const { data: todos } = useQuery(todosCollection.query());
|
|
1000
|
+
|
|
1001
|
+
const [optimisticTodos, setOptimisticTodos] = useState<Array<Todo>>([]);
|
|
1002
|
+
|
|
1003
|
+
const displayTodos = [...todos, ...optimisticTodos];
|
|
1004
|
+
|
|
1005
|
+
const addTodo = async (title: string) => {
|
|
1006
|
+
const newTodo = {
|
|
1007
|
+
id: crypto.randomUUID(),
|
|
1008
|
+
title,
|
|
1009
|
+
completed: false,
|
|
1010
|
+
};
|
|
1011
|
+
|
|
1012
|
+
// Optimistic update
|
|
1013
|
+
setOptimisticTodos(prev => [...prev, newTodo]);
|
|
1014
|
+
|
|
1015
|
+
try {
|
|
1016
|
+
await todosCollection.doc({ id: newTodo.id }).insert(newTodo);
|
|
1017
|
+
// Remove from optimistic list (real data will appear via subscription)
|
|
1018
|
+
setOptimisticTodos(prev => prev.filter(t => t.id !== newTodo.id));
|
|
1019
|
+
} catch (error) {
|
|
1020
|
+
// Revert optimistic update on error
|
|
1021
|
+
setOptimisticTodos(prev => prev.filter(t => t.id !== newTodo.id));
|
|
1022
|
+
// Show error to user
|
|
1023
|
+
}
|
|
1024
|
+
};
|
|
1025
|
+
|
|
1026
|
+
return <div>{displayTodos.map(/* ... */)}</div>;
|
|
1027
|
+
}
|
|
1028
|
+
```
|
|
1029
|
+
|
|
1030
|
+
## Best Practices
|
|
1031
|
+
|
|
1032
|
+
### 1. Always Use Dependencies Correctly
|
|
1033
|
+
|
|
1034
|
+
```typescript
|
|
1035
|
+
// GOOD: Include all values that affect the query
|
|
1036
|
+
const { data } = useQuery(
|
|
1037
|
+
collection.query().where('userId', '==', userId).where('status', '==', status),
|
|
1038
|
+
{},
|
|
1039
|
+
[userId, status], // Re-subscribe when these change
|
|
1040
|
+
);
|
|
1041
|
+
|
|
1042
|
+
// BAD: Missing dependencies can cause stale data
|
|
1043
|
+
const { data } = useQuery(
|
|
1044
|
+
collection.query().where('userId', '==', userId),
|
|
1045
|
+
{},
|
|
1046
|
+
[], // Missing userId - won't re-fetch when userId changes!
|
|
1047
|
+
);
|
|
1048
|
+
```
|
|
1049
|
+
|
|
1050
|
+
### 2. Use `enabled` for Conditional Fetching
|
|
1051
|
+
|
|
1052
|
+
```typescript
|
|
1053
|
+
// GOOD: Prevent unnecessary queries
|
|
1054
|
+
const { data } = useQuery(query, { enabled: !!userId }, [userId]);
|
|
1055
|
+
|
|
1056
|
+
// BAD: Query runs even when userId is null
|
|
1057
|
+
const { data } = useQuery(query, {}, [userId]);
|
|
1058
|
+
```
|
|
1059
|
+
|
|
1060
|
+
### 3. Provide Type Parameters
|
|
1061
|
+
|
|
1062
|
+
```typescript
|
|
1063
|
+
// GOOD: Full type safety
|
|
1064
|
+
interface User {
|
|
1065
|
+
id: string;
|
|
1066
|
+
name: string;
|
|
1067
|
+
email: string;
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
const collection = useCollection<User>('users');
|
|
1071
|
+
const { data } = useQuery(collection.query()); // data: Array<User>
|
|
1072
|
+
|
|
1073
|
+
// BAD: Loses type information
|
|
1074
|
+
const collection = useCollection('users'); // collection: CollectionReference<DocumentData>
|
|
1075
|
+
```
|
|
1076
|
+
|
|
1077
|
+
### 4. Handle Loading and Error States
|
|
1078
|
+
|
|
1079
|
+
```typescript
|
|
1080
|
+
// GOOD: Always handle all states
|
|
1081
|
+
const { loading, data, error } = useQuery(query);
|
|
1082
|
+
|
|
1083
|
+
if (loading) return <Spinner />;
|
|
1084
|
+
if (error) return <ErrorMessage error={error} />;
|
|
1085
|
+
if (!data.length) return <EmptyState />;
|
|
1086
|
+
|
|
1087
|
+
return <DataList data={data} />;
|
|
1088
|
+
|
|
1089
|
+
// BAD: Can cause runtime errors or poor UX
|
|
1090
|
+
const { data } = useQuery(query);
|
|
1091
|
+
return <DataList data={data} />; // data might be empty array during loading!
|
|
1092
|
+
```
|
|
1093
|
+
|
|
1094
|
+
### 5. Separate Read and Write Concerns
|
|
1095
|
+
|
|
1096
|
+
```typescript
|
|
1097
|
+
// GOOD: Hooks for reads, squid client for writes
|
|
1098
|
+
function Component() {
|
|
1099
|
+
const squid = useSquid();
|
|
1100
|
+
const collection = useCollection<Item>('items');
|
|
1101
|
+
|
|
1102
|
+
// Read with hook (reactive)
|
|
1103
|
+
const { data: items } = useQuery(collection.query());
|
|
1104
|
+
|
|
1105
|
+
// Write with squid client (imperative)
|
|
1106
|
+
const addItem = () => squid.collection<Item>('items').doc({ id: '...' }).insert({ ... });
|
|
1107
|
+
}
|
|
1108
|
+
```
|
|
1109
|
+
|
|
1110
|
+
### 6. Memoize Query Objects When Needed
|
|
1111
|
+
|
|
1112
|
+
```typescript
|
|
1113
|
+
// If building complex queries, memoize to prevent unnecessary re-renders
|
|
1114
|
+
function FilteredList({ filters }: { filters: Filters }) {
|
|
1115
|
+
const collection = useCollection<Item>('items');
|
|
1116
|
+
|
|
1117
|
+
const query = useMemo(() => {
|
|
1118
|
+
let q = collection.query();
|
|
1119
|
+
if (filters.status) q = q.where('status', '==', filters.status);
|
|
1120
|
+
if (filters.category) q = q.where('category', '==', filters.category);
|
|
1121
|
+
return q;
|
|
1122
|
+
}, [collection, filters.status, filters.category]);
|
|
1123
|
+
|
|
1124
|
+
const { data } = useQuery(query, {}, [filters.status, filters.category]);
|
|
1125
|
+
|
|
1126
|
+
return <div>{data.length} items</div>;
|
|
1127
|
+
}
|
|
1128
|
+
```
|
|
1129
|
+
|
|
1130
|
+
## TypeScript Types
|
|
1131
|
+
|
|
1132
|
+
All hooks and components are fully typed. Key types exported from `@squidcloud/react`:
|
|
1133
|
+
|
|
1134
|
+
```typescript
|
|
1135
|
+
// Context
|
|
1136
|
+
export type { SquidContextType, SquidContextProps };
|
|
1137
|
+
export { SquidContext, SquidContextProvider };
|
|
1138
|
+
|
|
1139
|
+
// Hooks
|
|
1140
|
+
export { useSquid };
|
|
1141
|
+
export { useCollection };
|
|
1142
|
+
export { useQuery, QueryType, QueryOptions };
|
|
1143
|
+
export { useDoc, DocType, DocOptions };
|
|
1144
|
+
export { useDocs, DocsType };
|
|
1145
|
+
export { usePagination, PaginationType, PaginationOptions };
|
|
1146
|
+
export { useObservable, ObservableType, ObservableOptions };
|
|
1147
|
+
export { usePromise, PromiseType, PromiseOptions };
|
|
1148
|
+
export { useQueue, QueueType, QueueOptions };
|
|
1149
|
+
|
|
1150
|
+
// AI Hooks
|
|
1151
|
+
export { useAiAgent, useAiChat, useAiQuery, useAiOnApi, useAskWithApi };
|
|
1152
|
+
export type { AiHookResponse, ChatMessage, AiChatMessage, UserChatMessage, CustomApiOptions };
|
|
1153
|
+
|
|
1154
|
+
// HOC
|
|
1155
|
+
export { withServerQuery };
|
|
1156
|
+
export type { WithQueryProps, WithQueryOptions };
|
|
1157
|
+
```
|
|
1158
|
+
|
|
1159
|
+
## Relationship with Squid Client SDK
|
|
1160
|
+
|
|
1161
|
+
`@squidcloud/react` is a thin wrapper around `@squidcloud/client`. Here's how they relate:
|
|
1162
|
+
|
|
1163
|
+
| React Hook | Client SDK Equivalent |
|
|
1164
|
+
|------------|----------------------|
|
|
1165
|
+
| `useSquid()` | `Squid.getInstance(options)` |
|
|
1166
|
+
| `useCollection(name)` | `squid.collection(name)` |
|
|
1167
|
+
| `useQuery(query)` | `query.snapshots()` / `query.snapshot()` |
|
|
1168
|
+
| `useDoc(docRef)` | `docRef.snapshots()` / `docRef.snapshot()` |
|
|
1169
|
+
| `usePagination(query)` | `query.paginate(options)` |
|
|
1170
|
+
| `useQueue(queue)` | `queue.consume()` / `queue.produce()` |
|
|
1171
|
+
| `useAiAgent(id)` | `squid.ai().agent(id)` |
|
|
1172
|
+
| `useObservable(obs)` | Direct RxJS subscription |
|
|
1173
|
+
|
|
1174
|
+
**When to use hooks vs. client SDK directly:**
|
|
1175
|
+
|
|
1176
|
+
- **Use hooks** for: Reading/subscribing to data in components
|
|
1177
|
+
- **Use client SDK** (via `useSquid()`) for: Mutations, backend functions, file uploads, transactions, and any operation that doesn't need reactive updates
|
|
1178
|
+
|
|
1179
|
+
## Common Mistakes to Avoid
|
|
1180
|
+
|
|
1181
|
+
### 1. Using Hooks Outside Provider
|
|
1182
|
+
|
|
1183
|
+
```typescript
|
|
1184
|
+
// ERROR: Will throw
|
|
1185
|
+
function App() {
|
|
1186
|
+
const squid = useSquid(); // Error: must be within SquidContextProvider
|
|
1187
|
+
return <div>...</div>;
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
// CORRECT
|
|
1191
|
+
function App() {
|
|
1192
|
+
return (
|
|
1193
|
+
<SquidContextProvider options={...}>
|
|
1194
|
+
<MyComponent />
|
|
1195
|
+
</SquidContextProvider>
|
|
1196
|
+
);
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
function MyComponent() {
|
|
1200
|
+
const squid = useSquid(); // Works!
|
|
1201
|
+
return <div>...</div>;
|
|
1202
|
+
}
|
|
1203
|
+
```
|
|
1204
|
+
|
|
1205
|
+
### 2. Forgetting to Handle Empty/Undefined Data
|
|
1206
|
+
|
|
1207
|
+
```typescript
|
|
1208
|
+
// BAD: data.map will fail during loading
|
|
1209
|
+
const { data } = useQuery(query);
|
|
1210
|
+
return data.map(item => ...);
|
|
1211
|
+
|
|
1212
|
+
// GOOD: Check loading or provide initialData
|
|
1213
|
+
const { loading, data } = useQuery(query, { initialData: [] });
|
|
1214
|
+
if (loading) return <Loading />;
|
|
1215
|
+
return data.map(item => ...);
|
|
1216
|
+
```
|
|
1217
|
+
|
|
1218
|
+
### 3. Creating Queries in Render
|
|
1219
|
+
|
|
1220
|
+
```typescript
|
|
1221
|
+
// BAD: Creates new query every render, causing infinite re-subscriptions
|
|
1222
|
+
function List({ userId }) {
|
|
1223
|
+
const collection = useCollection('items');
|
|
1224
|
+
const { data } = useQuery(
|
|
1225
|
+
collection.query().where('userId', '==', userId), // New object every render!
|
|
1226
|
+
{},
|
|
1227
|
+
[], // Empty deps won't help
|
|
1228
|
+
);
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1231
|
+
// GOOD: Use deps to control re-subscription
|
|
1232
|
+
function List({ userId }) {
|
|
1233
|
+
const collection = useCollection('items');
|
|
1234
|
+
const query = collection.query().where('userId', '==', userId);
|
|
1235
|
+
const { data } = useQuery(query, {}, [userId]); // Re-subscribe when userId changes
|
|
1236
|
+
}
|
|
1237
|
+
```
|
|
1238
|
+
|
|
1239
|
+
### 4. Missing RxJS Peer Dependency
|
|
1240
|
+
|
|
1241
|
+
```bash
|
|
1242
|
+
# If you see errors about Observable, install rxjs:
|
|
1243
|
+
npm install rxjs
|
|
1244
|
+
```
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@squidcloud/cli",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.424",
|
|
4
4
|
"description": "The Squid CLI",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
"node": ">=18.0.0"
|
|
29
29
|
},
|
|
30
30
|
"dependencies": {
|
|
31
|
-
"@squidcloud/local-backend": "^1.0.
|
|
31
|
+
"@squidcloud/local-backend": "^1.0.424",
|
|
32
32
|
"adm-zip": "^0.5.16",
|
|
33
33
|
"copy-webpack-plugin": "^12.0.2",
|
|
34
34
|
"decompress": "^4.2.1",
|