@squidcloud/cli 1.0.413 → 1.0.415
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.
|
@@ -0,0 +1,1849 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: Squid Development
|
|
3
|
+
description: Provides comprehensive, code-verified knowledge about developing applications with Squid. Use this skill proactively before writing, editing, or creating any code that uses Squid's Client SDK, Backend SDK, or CLI to ensure compliance with Squid's APIs and best practices.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Squid Development Skill (Verified)
|
|
7
|
+
|
|
8
|
+
This skill provides comprehensive, code-verified knowledge about developing applications with Squid.
|
|
9
|
+
|
|
10
|
+
## Overview
|
|
11
|
+
|
|
12
|
+
Squid is a backend-as-a-service platform that provides:
|
|
13
|
+
- **Client SDK** (`@squidcloud/client`) - TypeScript/JavaScript SDK for frontend
|
|
14
|
+
- **Backend SDK** (`@squidcloud/backend`) - TypeScript SDK with decorators for backend
|
|
15
|
+
- **CLI** (`@squidcloud/cli`) - Local development and deployment tools
|
|
16
|
+
- **Built-in integrations** - Databases, queues, storage, AI, APIs
|
|
17
|
+
|
|
18
|
+
## CLI Commands
|
|
19
|
+
|
|
20
|
+
### Installation
|
|
21
|
+
```bash
|
|
22
|
+
npm install -g @squidcloud/cli
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
### `squid init <project-name>`
|
|
26
|
+
Creates new backend project with `.env` and example service.
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
squid init backend --appId YOUR_APP_ID --apiKey YOUR_API_KEY --environmentId dev --squidDeveloperId YOUR_DEV_ID --region us-east-1.aws
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
### `squid start`
|
|
33
|
+
Runs backend locally with hot-reload, connects to Squid Cloud via reverse proxy.
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
cd backend
|
|
37
|
+
squid start
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
**Extended logging in `.env`:**
|
|
41
|
+
```env
|
|
42
|
+
SQUID_LOG_TYPES=QUERY,MUTATION,AI,API,ERROR
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### `squid deploy`
|
|
46
|
+
Builds and deploys to Squid Cloud.
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
squid deploy [--apiKey KEY] [--environmentId prod] [--skipBuild]
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### `squid build`
|
|
53
|
+
Builds backend project.
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
squid build [--dev] [--skip-version-check]
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Client SDK
|
|
60
|
+
|
|
61
|
+
### Where Can You Use the SDK?
|
|
62
|
+
|
|
63
|
+
The Squid Client SDK can be instantiated in various environments:
|
|
64
|
+
- **Web applications** (vanilla JavaScript, React, Vue, Angular, etc.)
|
|
65
|
+
- **Node.js servers** (Express, Fastify, etc.)
|
|
66
|
+
- **Mobile applications** (React Native, etc.)
|
|
67
|
+
- **Desktop applications** (Electron, etc.)
|
|
68
|
+
|
|
69
|
+
**Important:** When writing backend code that runs on Squid's infrastructure, the Squid client is **already initialized and available** for you (see Backend SDK section). You don't need to manually create a new instance.
|
|
70
|
+
|
|
71
|
+
### Initialization
|
|
72
|
+
|
|
73
|
+
#### Configuration Options
|
|
74
|
+
|
|
75
|
+
**Required Parameters:**
|
|
76
|
+
- **`appId`** (string): Your Squid application identifier. Can be found in the Squid Console.
|
|
77
|
+
- **`region`** (string): The region where your Squid application is deployed:
|
|
78
|
+
- AWS regions: `'us-east-1.aws'`, `'ap-south-1.aws'`, `'eu-central-1.aws'`
|
|
79
|
+
- GCP regions: `'us-central1.gcp'`
|
|
80
|
+
|
|
81
|
+
**Optional Parameters:**
|
|
82
|
+
- **`apiKey`** (string): API key that bypasses security rules and provides full administrative access.
|
|
83
|
+
- **IMPORTANT**: Has **complete control over your application** and bypasses all security rules
|
|
84
|
+
- Only use in trusted environments (backend servers, admin tools, development)
|
|
85
|
+
- **Never expose in client-side code** (web browsers, mobile apps)
|
|
86
|
+
- **`authProvider`** (object): Authentication provider that supplies OAuth2.0 tokens for the current user
|
|
87
|
+
- `integrationId` (string): The ID of your auth integration
|
|
88
|
+
- `getToken` (function): Returns a promise that resolves to the user's auth token (or undefined if not authenticated)
|
|
89
|
+
- Authentication details are **available in backend functions** via the execution context
|
|
90
|
+
- **`environmentId`** (string): Environment identifier (dev, staging, prod). Defaults to production.
|
|
91
|
+
- **`squidDeveloperId`** (string): Your developer identifier, used for local development and debugging.
|
|
92
|
+
- **`consoleRegion`** (string): Console region (optional)
|
|
93
|
+
|
|
94
|
+
#### Basic Setup
|
|
95
|
+
|
|
96
|
+
```typescript
|
|
97
|
+
import { Squid } from '@squidcloud/client';
|
|
98
|
+
|
|
99
|
+
const squid = new Squid({
|
|
100
|
+
appId: 'your-app-id',
|
|
101
|
+
region: 'us-east-1.aws',
|
|
102
|
+
environmentId: 'dev' // optional
|
|
103
|
+
});
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
#### Authentication: API Key vs Auth Provider
|
|
107
|
+
|
|
108
|
+
**Using API Key (Server-Side Only):**
|
|
109
|
+
```typescript
|
|
110
|
+
const squid = new Squid({
|
|
111
|
+
appId: 'your-app-id',
|
|
112
|
+
region: 'us-east-1.aws',
|
|
113
|
+
apiKey: 'your-api-key' // Full admin access
|
|
114
|
+
});
|
|
115
|
+
```
|
|
116
|
+
- **Use cases**: Backend servers, admin tools, scripts, development environments
|
|
117
|
+
- **Security**: Has full control - bypasses all security rules
|
|
118
|
+
- **Warning**: Never use in client-side code
|
|
119
|
+
|
|
120
|
+
**Using Auth Provider (Client-Side):**
|
|
121
|
+
```typescript
|
|
122
|
+
const squid = new Squid({
|
|
123
|
+
appId: 'your-app-id',
|
|
124
|
+
region: 'us-east-1.aws',
|
|
125
|
+
authProvider: {
|
|
126
|
+
integrationId: 'auth-integration-id',
|
|
127
|
+
getToken: async () => {
|
|
128
|
+
// Return user's OAuth token
|
|
129
|
+
return await getCurrentUserToken();
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
```
|
|
134
|
+
- **Use cases**: Web apps, mobile apps, any user-facing application
|
|
135
|
+
- **Security**: Respects security rules based on authenticated user
|
|
136
|
+
- **Backend access**: User authentication details are available in backend functions through the execution context
|
|
137
|
+
|
|
138
|
+
**Setting Auth Provider After Initialization:**
|
|
139
|
+
```typescript
|
|
140
|
+
const squid = new Squid({
|
|
141
|
+
appId: 'your-app-id',
|
|
142
|
+
region: 'us-east-1.aws'
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
// Later, when user logs in
|
|
146
|
+
squid.setAuthProvider({
|
|
147
|
+
integrationId: 'auth-integration-id',
|
|
148
|
+
getToken: async () => userAuthToken
|
|
149
|
+
});
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### Database & Data Management
|
|
153
|
+
|
|
154
|
+
Squid provides database functionality similar to Firestore but more powerful, with collections, document references, and real-time capabilities. Squid supports multiple database types including NoSQL databases (like MongoDB) and relational databases (like PostgreSQL, MySQL).
|
|
155
|
+
|
|
156
|
+
#### Security Rules
|
|
157
|
+
|
|
158
|
+
Every database operation in Squid can be secured using **security rules**. Security rules are backend functions decorated with `@secureDatabase` or `@secureCollection` that contain business logic to determine whether an operation is allowed.
|
|
159
|
+
|
|
160
|
+
```typescript
|
|
161
|
+
// Example security rule for a collection (read operations)
|
|
162
|
+
@secureCollection('users', 'read')
|
|
163
|
+
async canReadUsers(context: QueryContext<User>): Promise<boolean> {
|
|
164
|
+
// Your business logic here
|
|
165
|
+
// context.collectionName, context.integrationId, context.limit, etc.
|
|
166
|
+
return true; // or false based on your logic
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Example security rule for mutations (insert, update, delete)
|
|
170
|
+
@secureCollection('users', 'update')
|
|
171
|
+
async canUpdateUsers(context: MutationContext): Promise<boolean> {
|
|
172
|
+
// Access document before and after the mutation
|
|
173
|
+
const before = context.before;
|
|
174
|
+
const after = context.after;
|
|
175
|
+
|
|
176
|
+
// Check if specific paths were affected
|
|
177
|
+
if (context.affectsPath('email')) {
|
|
178
|
+
return false; // Don't allow email changes
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return true;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Database-wide security rule
|
|
185
|
+
@secureDatabase('insert', 'my-integration-id')
|
|
186
|
+
async canInsertAnywhere(context: MutationContext): Promise<boolean> {
|
|
187
|
+
// Your business logic here
|
|
188
|
+
return context.after?.createdBy === 'admin';
|
|
189
|
+
}
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
These decorated functions return a boolean (or `Promise<boolean>`) indicating whether the operation is allowed. The developer has full control to implement any business logic needed for authorization.
|
|
193
|
+
|
|
194
|
+
**Context types:**
|
|
195
|
+
- `QueryContext` - Used for 'read' and 'all' operations (exported from `@squidcloud/backend`)
|
|
196
|
+
- `MutationContext` - Used for 'insert', 'update', 'delete' operations (exported from `@squidcloud/backend`)
|
|
197
|
+
|
|
198
|
+
Security decorators (`@secureDatabase`, `@secureCollection`) are exported from `@squidcloud/backend`.
|
|
199
|
+
|
|
200
|
+
#### Collection Access
|
|
201
|
+
|
|
202
|
+
A collection represents a set of documents (similar to a table in relational databases or a collection in NoSQL databases).
|
|
203
|
+
|
|
204
|
+
```typescript
|
|
205
|
+
// Get collection reference in the built-in database
|
|
206
|
+
const users = squid.collection<User>('users');
|
|
207
|
+
|
|
208
|
+
// Get collection reference in a specific integration
|
|
209
|
+
const orders = squid.collection<Order>('orders', 'postgres-db');
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
**Type parameter**: The generic type `<T>` defines the structure of documents in the collection.
|
|
213
|
+
|
|
214
|
+
**Document References - The `doc()` method has different behaviors:**
|
|
215
|
+
|
|
216
|
+
**1. No parameters - `doc()`**
|
|
217
|
+
Generates a new document ID (useful for creating new documents):
|
|
218
|
+
```typescript
|
|
219
|
+
// For built_in_db without schema - generates a random ID
|
|
220
|
+
const newDocRef = collection.doc();
|
|
221
|
+
await newDocRef.insert({ name: 'John', age: 30 });
|
|
222
|
+
|
|
223
|
+
// For other integrations - creates a placeholder that gets resolved on insert
|
|
224
|
+
const newDocRef = collection.doc();
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
**2. String parameter - `doc(stringId)`**
|
|
228
|
+
**Only supported for `built_in_db` integration without a defined schema:**
|
|
229
|
+
```typescript
|
|
230
|
+
// Valid only for built_in_db collections without schema
|
|
231
|
+
const docRef = collection.doc('my-doc-id');
|
|
232
|
+
const docRef = collection.doc('user-12345');
|
|
233
|
+
```
|
|
234
|
+
**Important**: For collections with defined schemas or non-built_in_db integrations, you must use object format.
|
|
235
|
+
|
|
236
|
+
**3. Object parameter - `doc({id: value})`**
|
|
237
|
+
Used for collections with defined primary keys. The object maps primary key field names to their values:
|
|
238
|
+
```typescript
|
|
239
|
+
// Single primary key field "id"
|
|
240
|
+
const docRef = collection.doc({ id: 'user-123' });
|
|
241
|
+
|
|
242
|
+
// Single primary key field "userId"
|
|
243
|
+
const docRef = collection.doc({ userId: 42 });
|
|
244
|
+
|
|
245
|
+
// Composite primary key (id1, id2)
|
|
246
|
+
const docRef = collection.doc({ id1: 'part1', id2: 'part2' });
|
|
247
|
+
|
|
248
|
+
// Partial primary key - missing fields generated on server if supported
|
|
249
|
+
const docRef = collection.doc({ id1: 'part1' }); // id2 generated on insert
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
**Key points about document IDs:**
|
|
253
|
+
- For `built_in_db` without schema: can use string IDs or object format
|
|
254
|
+
- For `built_in_db` with schema: must use object format matching primary key fields
|
|
255
|
+
- For other integrations: must use object format matching primary key fields
|
|
256
|
+
- Partial primary keys: missing fields may be auto-generated on insert (if integration supports it)
|
|
257
|
+
- Empty `doc()`: generates a placeholder ID that gets resolved when document is created
|
|
258
|
+
|
|
259
|
+
```typescript
|
|
260
|
+
// Examples:
|
|
261
|
+
|
|
262
|
+
// Insert
|
|
263
|
+
await userDoc.insert({ name: 'John', email: 'john@example.com' });
|
|
264
|
+
|
|
265
|
+
// Update (partial)
|
|
266
|
+
await userDoc.update({ name: 'Jane' });
|
|
267
|
+
|
|
268
|
+
// Update specific path
|
|
269
|
+
await userDoc.setInPath('address.street', 'Main St');
|
|
270
|
+
|
|
271
|
+
// Delete specific path
|
|
272
|
+
await userDoc.deleteInPath('tempData');
|
|
273
|
+
|
|
274
|
+
// Delete document
|
|
275
|
+
await userDoc.delete();
|
|
276
|
+
|
|
277
|
+
// Get data (promise)
|
|
278
|
+
const userData = await userDoc.snapshot();
|
|
279
|
+
console.log(userData);
|
|
280
|
+
|
|
281
|
+
// Get data (observable - realtime)
|
|
282
|
+
userDoc.snapshots().subscribe(data => {
|
|
283
|
+
console.log(data);
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
// Get cached data (no server fetch)
|
|
287
|
+
const cached = userDoc.peek();
|
|
288
|
+
|
|
289
|
+
// Bulk operations
|
|
290
|
+
await users.insertMany([
|
|
291
|
+
{ id: 'user-1', data: { name: 'Alice' } },
|
|
292
|
+
{ id: 'user-2', data: { name: 'Bob' } }
|
|
293
|
+
]);
|
|
294
|
+
|
|
295
|
+
await users.deleteMany(['user-1', 'user-2']);
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
#### Real-time Subscriptions
|
|
299
|
+
|
|
300
|
+
Squid provides real-time data synchronization similar to Firestore. Subscribe to document or query changes and receive updates automatically.
|
|
301
|
+
|
|
302
|
+
**Document Subscriptions:**
|
|
303
|
+
```typescript
|
|
304
|
+
// Subscribe to document changes
|
|
305
|
+
const subscription = docRef.snapshots().subscribe((userData) => {
|
|
306
|
+
if (userData) {
|
|
307
|
+
console.log('Document updated:', userData);
|
|
308
|
+
} else {
|
|
309
|
+
console.log('Document deleted or does not exist');
|
|
310
|
+
}
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
// Unsubscribe when done
|
|
314
|
+
subscription.unsubscribe();
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
**Query Subscriptions:**
|
|
318
|
+
```typescript
|
|
319
|
+
// Subscribe to query results - ALWAYS use dereference()
|
|
320
|
+
const subscription = collection
|
|
321
|
+
.query()
|
|
322
|
+
.eq('status', 'active')
|
|
323
|
+
.gte('age', 18)
|
|
324
|
+
.dereference() // Important: Converts DocumentReferences to actual data
|
|
325
|
+
.snapshots()
|
|
326
|
+
.subscribe((users) => {
|
|
327
|
+
console.log('Active users updated:', users);
|
|
328
|
+
// users is Array<User> with actual data
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
// Unsubscribe when done
|
|
332
|
+
subscription.unsubscribe();
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
**Real-time Updates:**
|
|
336
|
+
When data changes on the server (from any client or backend operation):
|
|
337
|
+
- Document subscriptions receive the updated document data
|
|
338
|
+
- Query subscriptions receive the updated query results
|
|
339
|
+
- Updates are pushed to clients via WebSocket connections
|
|
340
|
+
- Changes are automatically reflected in the local data
|
|
341
|
+
|
|
342
|
+
**Important**: Always unsubscribe from subscriptions when they're no longer needed to prevent memory leaks.
|
|
343
|
+
|
|
344
|
+
### Database - Queries
|
|
345
|
+
|
|
346
|
+
**ALL Available Operators:**
|
|
347
|
+
- Comparison: `eq`, `neq`, `gt`, `gte`, `lt`, `lte`
|
|
348
|
+
- Arrays: `in`, `nin`, `arrayIncludesSome`, `arrayIncludesAll`, `arrayNotIncludes`
|
|
349
|
+
- Patterns: `like`, `notLike` (% = any chars, _ = one char)
|
|
350
|
+
|
|
351
|
+
```typescript
|
|
352
|
+
// Basic query
|
|
353
|
+
const activeUsers = await users.query()
|
|
354
|
+
.eq('status', 'active')
|
|
355
|
+
.gt('age', 18)
|
|
356
|
+
.sortBy('name', true) // true = ascending
|
|
357
|
+
.limit(100) // max 20000, default 1000
|
|
358
|
+
.snapshot();
|
|
359
|
+
|
|
360
|
+
activeUsers.data.forEach(doc => {
|
|
361
|
+
console.log(doc.data); // Document data
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
// Realtime query
|
|
365
|
+
users.query()
|
|
366
|
+
.eq('status', 'active')
|
|
367
|
+
.snapshots().subscribe(snapshot => {
|
|
368
|
+
snapshot.data.forEach(doc => console.log(doc.data));
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
// Pattern matching
|
|
372
|
+
const results = await users.query()
|
|
373
|
+
.like('email', '%.com') // case-insensitive by default
|
|
374
|
+
.snapshot();
|
|
375
|
+
|
|
376
|
+
// Array operators
|
|
377
|
+
const tagged = await posts.query()
|
|
378
|
+
.in('category', ['tech', 'news'])
|
|
379
|
+
.arrayIncludesSome('tags', ['urgent', 'important'])
|
|
380
|
+
.snapshot();
|
|
381
|
+
|
|
382
|
+
// Using where() method (all operators above are shortcuts for where)
|
|
383
|
+
const results1 = await users.query().eq('status', 'active').snapshot();
|
|
384
|
+
const results2 = await users.query().where('status', '==', 'active').snapshot();
|
|
385
|
+
// These are equivalent
|
|
386
|
+
|
|
387
|
+
// OR queries - combine multiple queries with OR logic
|
|
388
|
+
const query1 = users.query().eq('status', 'active');
|
|
389
|
+
const query2 = users.query().eq('status', 'pending');
|
|
390
|
+
const orResults = await users.or(query1, query2)
|
|
391
|
+
.dereference()
|
|
392
|
+
.snapshot();
|
|
393
|
+
// Returns documents matching either query
|
|
394
|
+
// Note: Results are merged and deduplicated
|
|
395
|
+
|
|
396
|
+
// Multiple sort
|
|
397
|
+
const sorted = await users.query()
|
|
398
|
+
.sortBy('lastName', true)
|
|
399
|
+
.sortBy('firstName', true)
|
|
400
|
+
.limit(50)
|
|
401
|
+
.snapshot();
|
|
402
|
+
|
|
403
|
+
// Join Queries - combine data from multiple collections
|
|
404
|
+
// Start with joinQuery() and alias
|
|
405
|
+
const results = await teachers
|
|
406
|
+
.joinQuery('teacher') // Alias for teachers collection
|
|
407
|
+
.join(
|
|
408
|
+
students.query(),
|
|
409
|
+
'students', // Alias for students collection
|
|
410
|
+
{ left: 'id', right: 'teacherId' } // Join condition
|
|
411
|
+
)
|
|
412
|
+
.dereference() // Important: converts refs to actual data
|
|
413
|
+
.snapshot();
|
|
414
|
+
// Results: Array<{ teacher: Teacher, students?: Student }>
|
|
415
|
+
|
|
416
|
+
// Inner join (only matching records)
|
|
417
|
+
const innerResults = await teachers
|
|
418
|
+
.joinQuery('teacher')
|
|
419
|
+
.join(
|
|
420
|
+
students.query(),
|
|
421
|
+
'students',
|
|
422
|
+
{ left: 'id', right: 'teacherId' },
|
|
423
|
+
{ isInner: true } // Inner join option
|
|
424
|
+
)
|
|
425
|
+
.dereference()
|
|
426
|
+
.snapshot();
|
|
427
|
+
|
|
428
|
+
// Multi-level joins
|
|
429
|
+
const threeWay = await collection1
|
|
430
|
+
.joinQuery('a')
|
|
431
|
+
.join(collection2.query(), 'b', { left: 'id', right: 'parentId' })
|
|
432
|
+
.join(collection3.query(), 'c', { left: 'id', right: 'grandParentId' })
|
|
433
|
+
.dereference()
|
|
434
|
+
.snapshot();
|
|
435
|
+
|
|
436
|
+
// Grouped joins (nests one-to-many as arrays)
|
|
437
|
+
const grouped = await teachers
|
|
438
|
+
.joinQuery('teacher')
|
|
439
|
+
.join(students.query(), 'students', { left: 'id', right: 'teacherId' })
|
|
440
|
+
.grouped()
|
|
441
|
+
.dereference()
|
|
442
|
+
.snapshot();
|
|
443
|
+
// Results: Array<{ teacher: Teacher, students: Student[] }>
|
|
444
|
+
|
|
445
|
+
// Dereference - Converts DocumentReferences to actual data
|
|
446
|
+
// WITHOUT dereference: returns Array<DocumentReference<User>>
|
|
447
|
+
const refs = await users.query().eq('active', true).snapshot();
|
|
448
|
+
// refs.data[0].data to access actual data
|
|
449
|
+
|
|
450
|
+
// WITH dereference: returns Array<User> directly
|
|
451
|
+
const userData = await users.query()
|
|
452
|
+
.eq('active', true)
|
|
453
|
+
.dereference()
|
|
454
|
+
.snapshot();
|
|
455
|
+
// userData[0] is the actual user object
|
|
456
|
+
|
|
457
|
+
// ALWAYS use dereference() for:
|
|
458
|
+
// - Real-time subscriptions (makes working with data easier)
|
|
459
|
+
// - When you need document data directly
|
|
460
|
+
// DON'T use dereference() if:
|
|
461
|
+
// - You need DocumentReference methods like .update() or .delete()
|
|
462
|
+
|
|
463
|
+
// Pagination
|
|
464
|
+
const pagination = users.query()
|
|
465
|
+
.sortBy('createdAt', false)
|
|
466
|
+
.dereference()
|
|
467
|
+
.paginate({
|
|
468
|
+
pageSize: 50, // Number of items per page (default: 100)
|
|
469
|
+
subscribe: true // Subscribe to real-time updates (default: true)
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
const firstPage = await pagination.first(); // Jump to first page
|
|
473
|
+
const nextPage = await pagination.next(); // Go to next page
|
|
474
|
+
const prevPage = await pagination.prev(); // Go to previous page
|
|
475
|
+
|
|
476
|
+
// Check pagination state
|
|
477
|
+
console.log(firstPage.hasNext); // boolean
|
|
478
|
+
console.log(firstPage.hasPrev); // boolean
|
|
479
|
+
|
|
480
|
+
// Watch changes
|
|
481
|
+
users.query()
|
|
482
|
+
.eq('status', 'active')
|
|
483
|
+
.changes()
|
|
484
|
+
.subscribe(changes => {
|
|
485
|
+
console.log('Inserts:', changes.inserts);
|
|
486
|
+
console.log('Updates:', changes.updates);
|
|
487
|
+
console.log('Deletes:', changes.deletes);
|
|
488
|
+
});
|
|
489
|
+
```
|
|
490
|
+
|
|
491
|
+
**Note:** `offset()` does NOT exist - use `paginate()` for pagination.
|
|
492
|
+
|
|
493
|
+
### Database - Transactions
|
|
494
|
+
|
|
495
|
+
```typescript
|
|
496
|
+
await squid.runInTransaction(async (txId) => {
|
|
497
|
+
const userRef = squid.collection('users').doc('user-1');
|
|
498
|
+
const accountRef = squid.collection('accounts').doc('acc-1');
|
|
499
|
+
|
|
500
|
+
// Pass txId as last parameter to each operation
|
|
501
|
+
// Use incrementInPath/decrementInPath for numeric operations
|
|
502
|
+
await userRef.decrementInPath('balance', 100, txId);
|
|
503
|
+
await accountRef.incrementInPath('balance', 100, txId);
|
|
504
|
+
|
|
505
|
+
// Both commit together or rollback together
|
|
506
|
+
});
|
|
507
|
+
```
|
|
508
|
+
|
|
509
|
+
### Database - Numeric Operations
|
|
510
|
+
|
|
511
|
+
```typescript
|
|
512
|
+
// Increment/decrement
|
|
513
|
+
await userDoc.incrementInPath('loginCount', 1);
|
|
514
|
+
await userDoc.decrementInPath('credits', 50);
|
|
515
|
+
|
|
516
|
+
// For arrays/objects, use update() with full new value
|
|
517
|
+
const currentData = await userDoc.snapshot();
|
|
518
|
+
await userDoc.update({
|
|
519
|
+
tags: [...currentData.tags, 'newTag'],
|
|
520
|
+
notifications: [...currentData.notifications, { msg: 'Hi' }]
|
|
521
|
+
});
|
|
522
|
+
```
|
|
523
|
+
|
|
524
|
+
### Database - Native Queries
|
|
525
|
+
|
|
526
|
+
```typescript
|
|
527
|
+
// SQL (PostgreSQL, MySQL, etc.) - uses ${param} syntax
|
|
528
|
+
const users = await squid.executeNativeRelationalQuery<User[]>(
|
|
529
|
+
'postgres-db',
|
|
530
|
+
'SELECT * FROM users WHERE age > ${minAge}',
|
|
531
|
+
{ minAge: 18 }
|
|
532
|
+
);
|
|
533
|
+
|
|
534
|
+
// MongoDB aggregation
|
|
535
|
+
const orders = await squid.executeNativeMongoQuery<Order[]>(
|
|
536
|
+
'mongo-db',
|
|
537
|
+
'orders',
|
|
538
|
+
[
|
|
539
|
+
{ $match: { status: 'completed' } },
|
|
540
|
+
{ $group: { _id: '$userId', total: { $sum: '$amount' } } }
|
|
541
|
+
]
|
|
542
|
+
);
|
|
543
|
+
|
|
544
|
+
// Elasticsearch
|
|
545
|
+
const products = await squid.executeNativeElasticQuery(
|
|
546
|
+
'elastic-db',
|
|
547
|
+
'products',
|
|
548
|
+
{ query: { match: { name: 'laptop' } } },
|
|
549
|
+
'_search', // endpoint (optional)
|
|
550
|
+
'GET' // method (optional)
|
|
551
|
+
);
|
|
552
|
+
|
|
553
|
+
// Pure (Finos Legend Pure Language) - uses ${param} syntax
|
|
554
|
+
const items = await squid.executeNativePureQuery(
|
|
555
|
+
'my-db',
|
|
556
|
+
'from products->filter(p | $p.price < ${maxPrice})',
|
|
557
|
+
{ maxPrice: 1000 }
|
|
558
|
+
);
|
|
559
|
+
```
|
|
560
|
+
|
|
561
|
+
### Database - Security (@secureDatabase, @secureCollection)
|
|
562
|
+
|
|
563
|
+
Secure your database operations with backend decorators. These are backend-only decorators that define who can access your data.
|
|
564
|
+
|
|
565
|
+
**Backend - Secure entire database:**
|
|
566
|
+
```typescript
|
|
567
|
+
import { SquidService, secureDatabase, QueryContext, MutationContext } from '@squidcloud/backend';
|
|
568
|
+
|
|
569
|
+
export class MyService extends SquidService {
|
|
570
|
+
@secureDatabase('read')
|
|
571
|
+
allowRead(context: QueryContext<User>): boolean {
|
|
572
|
+
const userId = this.getUserAuth()?.userId;
|
|
573
|
+
if (!userId) return false;
|
|
574
|
+
return context.isSubqueryOf('userId', '==', userId);
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
@secureDatabase('write')
|
|
578
|
+
allowWrite(context: MutationContext<User>): boolean {
|
|
579
|
+
return this.isAuthenticated();
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
@secureDatabase('all')
|
|
583
|
+
allowAll(): boolean {
|
|
584
|
+
return this.isAuthenticated();
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
```
|
|
588
|
+
|
|
589
|
+
**Types:** `'read'`, `'write'`, `'update'`, `'insert'`, `'delete'`, `'all'`
|
|
590
|
+
|
|
591
|
+
**Backend - Secure specific collection:**
|
|
592
|
+
```typescript
|
|
593
|
+
// QueryContext methods (for 'read' operations)
|
|
594
|
+
@secureCollection('users', 'read')
|
|
595
|
+
allowUserRead(context: QueryContext): boolean {
|
|
596
|
+
const userId = this.getUserAuth()?.userId;
|
|
597
|
+
if (!userId) return false;
|
|
598
|
+
// Check if query filters on a specific field
|
|
599
|
+
return context.isSubqueryOf('id', '==', userId);
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
// MutationContext methods (for 'insert', 'update', 'delete' operations)
|
|
603
|
+
@secureCollection('users', 'update')
|
|
604
|
+
allowUserUpdate(context: MutationContext<User>): boolean {
|
|
605
|
+
const userId = this.getUserAuth()?.userId;
|
|
606
|
+
if (!userId) return false;
|
|
607
|
+
|
|
608
|
+
// context.before: document state before mutation
|
|
609
|
+
// context.after: document state after mutation
|
|
610
|
+
|
|
611
|
+
// Check if specific paths were affected
|
|
612
|
+
if (context.affectsPath('email')) {
|
|
613
|
+
return false; // Don't allow email changes
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
// Check ownership
|
|
617
|
+
return context.after?.id === userId;
|
|
618
|
+
}
|
|
619
|
+
```
|
|
620
|
+
|
|
621
|
+
**Context types (exported from `@squidcloud/backend`):**
|
|
622
|
+
- `QueryContext<T>` - Used for 'read' and 'all' operations
|
|
623
|
+
- `isSubqueryOf(field, operator, value)` - Check if query filters on field
|
|
624
|
+
- `collectionName`, `integrationId`, `limit` properties
|
|
625
|
+
- `MutationContext<T>` - Used for 'insert', 'update', 'delete' operations
|
|
626
|
+
- `affectsPath(path)` - Check if specific field path was modified
|
|
627
|
+
- `before` - Document before mutation (undefined for insert)
|
|
628
|
+
- `after` - Document after mutation (undefined for delete)
|
|
629
|
+
|
|
630
|
+
### Backend Functions (@executable)
|
|
631
|
+
|
|
632
|
+
Backend functions allow you to write custom server-side logic that can be called from the client.
|
|
633
|
+
|
|
634
|
+
**Backend - Define the function:**
|
|
635
|
+
```typescript
|
|
636
|
+
import { SquidService, executable, SquidFile } from '@squidcloud/backend';
|
|
637
|
+
|
|
638
|
+
export class MyService extends SquidService {
|
|
639
|
+
@executable()
|
|
640
|
+
async greetUser(name: string): Promise<string> {
|
|
641
|
+
return `Hello, ${name}`;
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
@executable()
|
|
645
|
+
async uploadFile(file: SquidFile): Promise<Result> {
|
|
646
|
+
console.log(file.originalName, file.size, file.data);
|
|
647
|
+
return { success: true };
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
```
|
|
651
|
+
|
|
652
|
+
**Client - Call the function:**
|
|
653
|
+
```typescript
|
|
654
|
+
// Execute @executable() decorated function
|
|
655
|
+
const result = await squid.executeFunction('greetUser', 'John');
|
|
656
|
+
const typedResult = await squid.executeFunction<string>('greetUser', 'John');
|
|
657
|
+
|
|
658
|
+
// With headers
|
|
659
|
+
const result = await squid.executeFunctionWithHeaders(
|
|
660
|
+
'processPayment',
|
|
661
|
+
{ 'X-Custom-Header': 'value' },
|
|
662
|
+
paymentData
|
|
663
|
+
);
|
|
664
|
+
```
|
|
665
|
+
|
|
666
|
+
### Webhooks (@webhook)
|
|
667
|
+
|
|
668
|
+
Webhooks allow you to create publicly accessible HTTP endpoints that can receive data from external services.
|
|
669
|
+
|
|
670
|
+
**Backend - Define the webhook:**
|
|
671
|
+
```typescript
|
|
672
|
+
import { SquidService, webhook, WebhookRequest } from '@squidcloud/backend';
|
|
673
|
+
|
|
674
|
+
export class MyService extends SquidService {
|
|
675
|
+
@webhook('github-events')
|
|
676
|
+
async handleGithub(request: WebhookRequest): Promise<any> {
|
|
677
|
+
console.log(request.body);
|
|
678
|
+
console.log(request.headers);
|
|
679
|
+
console.log(request.queryParams);
|
|
680
|
+
console.log(request.httpMethod); // 'post' | 'get' | 'put' | 'delete'
|
|
681
|
+
console.log(request.files);
|
|
682
|
+
|
|
683
|
+
return this.createWebhookResponse({ received: true }, 200);
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
```
|
|
687
|
+
|
|
688
|
+
**Webhook URL:** `https://<appId>.<region>.squid.cloud/webhooks/<webhook-id>`
|
|
689
|
+
|
|
690
|
+
**Client - Call webhook programmatically (optional):**
|
|
691
|
+
```typescript
|
|
692
|
+
// Usually webhooks are called by external services, but you can also call them from client
|
|
693
|
+
const result = await squid.executeWebhook<Response>('github-events', {
|
|
694
|
+
headers: { 'X-GitHub-Event': 'push' },
|
|
695
|
+
queryParams: { ref: 'main' },
|
|
696
|
+
body: { commits: [...] },
|
|
697
|
+
files: [file1, file2]
|
|
698
|
+
});
|
|
699
|
+
```
|
|
700
|
+
|
|
701
|
+
### AI - Supported Models
|
|
702
|
+
|
|
703
|
+
Squid supports multiple AI providers and models for different use cases:
|
|
704
|
+
|
|
705
|
+
**Chat Models** (for AI Agents, AI Query, etc.):
|
|
706
|
+
|
|
707
|
+
*OpenAI:*
|
|
708
|
+
- `o1`, `o3`, `o3-mini`, `o4-mini`
|
|
709
|
+
- `gpt-5`, `gpt-5-mini`, `gpt-5-nano`
|
|
710
|
+
- `gpt-4.1` (default), `gpt-4.1-mini`, `gpt-4.1-nano`
|
|
711
|
+
- `gpt-4o`, `gpt-4o-mini`
|
|
712
|
+
|
|
713
|
+
*Anthropic (Claude):*
|
|
714
|
+
- `claude-3-7-sonnet-latest`
|
|
715
|
+
- `claude-haiku-4-5-20251001`
|
|
716
|
+
- `claude-opus-4-20250514`, `claude-opus-4-1-20250805`
|
|
717
|
+
- `claude-sonnet-4-20250514`, `claude-sonnet-4-5-20250929`
|
|
718
|
+
|
|
719
|
+
*Google (Gemini):*
|
|
720
|
+
- `gemini-2.5-pro`, `gemini-2.5-flash`, `gemini-2.5-flash-lite`
|
|
721
|
+
|
|
722
|
+
*xAI (Grok):*
|
|
723
|
+
- `grok-3`, `grok-3-fast`, `grok-3-mini`, `grok-3-mini-fast`
|
|
724
|
+
- `grok-4`, `grok-4-fast-reasoning`, `grok-4-fast-non-reasoning`
|
|
725
|
+
- Note: `grok-3-mini` is ~10x less expensive than `grok-3`
|
|
726
|
+
- Note: `*-fast` models are ~2x more expensive and only marginally faster
|
|
727
|
+
|
|
728
|
+
**Embedding Models** (for Knowledge Bases):
|
|
729
|
+
- `text-embedding-3-small` (OpenAI, default) - 8,192 token limit
|
|
730
|
+
- `voyage-3-large` (Voyage) - 32,000 token limit
|
|
731
|
+
|
|
732
|
+
**Image Generation Models:**
|
|
733
|
+
- `dall-e-3` (OpenAI)
|
|
734
|
+
- `stable-diffusion-core` (Stability)
|
|
735
|
+
- `flux-pro-1.1`, `flux-kontext-pro` (Flux)
|
|
736
|
+
|
|
737
|
+
**Audio Models:**
|
|
738
|
+
|
|
739
|
+
*Transcription:*
|
|
740
|
+
- `whisper-1`
|
|
741
|
+
- `gpt-4o-transcribe`, `gpt-4o-mini-transcribe`
|
|
742
|
+
|
|
743
|
+
*Text-to-Speech:*
|
|
744
|
+
- `tts-1`, `tts-1-hd`, `gpt-4o-mini-tts`
|
|
745
|
+
|
|
746
|
+
### AI - Agents
|
|
747
|
+
|
|
748
|
+
AI Agents are powerful, configurable AI assistants that can interact with your data, call functions, connect to integrations, and collaborate with other agents.
|
|
749
|
+
|
|
750
|
+
**An AI Agent can:**
|
|
751
|
+
- Maintain conversation history (**memory is enabled by default**)
|
|
752
|
+
- Call backend functions decorated with `@aiFunction`
|
|
753
|
+
- Query databases and APIs through connected integrations
|
|
754
|
+
- Search knowledge bases for relevant information
|
|
755
|
+
- Collaborate with other AI agents
|
|
756
|
+
- Process voice input and generate voice output
|
|
757
|
+
- Accept files as part of chat requests
|
|
758
|
+
|
|
759
|
+
**Creating and Managing Agents:**
|
|
760
|
+
|
|
761
|
+
```typescript
|
|
762
|
+
const aiClient = squid.ai();
|
|
763
|
+
|
|
764
|
+
// Get the built-in agent (no ID required)
|
|
765
|
+
const builtInAgent = aiClient.agent();
|
|
766
|
+
|
|
767
|
+
// Get a custom agent by ID
|
|
768
|
+
const myAgent = aiClient.agent('my-agent-id');
|
|
769
|
+
|
|
770
|
+
// Get an agent with an Agent API key (allows calling without App API key, bypasses @secureAiAgent methods)
|
|
771
|
+
const agentWithKey = aiClient.agent('my-agent-id', {
|
|
772
|
+
apiKey: 'your-agent-api-key'
|
|
773
|
+
});
|
|
774
|
+
|
|
775
|
+
// Create or update an agent
|
|
776
|
+
await myAgent.upsert({
|
|
777
|
+
description: 'Customer support assistant',
|
|
778
|
+
isPublic: false, // Whether the agent is publicly accessible
|
|
779
|
+
auditLog: true, // Enable audit logging for compliance
|
|
780
|
+
options: {
|
|
781
|
+
model: 'gpt-4o', // or 'claude-sonnet-4-5-20250929', 'gemini-2.5-flash'
|
|
782
|
+
instructions: 'You are a helpful customer support assistant. Be concise and professional.',
|
|
783
|
+
temperature: 0.7
|
|
784
|
+
}
|
|
785
|
+
});
|
|
786
|
+
|
|
787
|
+
// Get agent details
|
|
788
|
+
const agentInfo = await myAgent.get();
|
|
789
|
+
console.log(agentInfo.id, agentInfo.description, agentInfo.options.model);
|
|
790
|
+
|
|
791
|
+
// Update specific properties
|
|
792
|
+
await myAgent.updateInstructions('You are a technical support specialist.');
|
|
793
|
+
await myAgent.updateModel('claude-sonnet-4-5-20250929');
|
|
794
|
+
await myAgent.updateGuardrails(['no-harmful-content']);
|
|
795
|
+
|
|
796
|
+
// Delete an agent
|
|
797
|
+
await myAgent.delete();
|
|
798
|
+
|
|
799
|
+
// List all agents
|
|
800
|
+
const agents = await squid.ai().listAgents();
|
|
801
|
+
agents.forEach(agent => console.log(`${agent.id}: ${agent.description}`));
|
|
802
|
+
```
|
|
803
|
+
|
|
804
|
+
**Agent Lifecycle:**
|
|
805
|
+
1. **Creation** - Use `upsert()` to create a new agent with an ID
|
|
806
|
+
2. **Active** - Agent can process requests
|
|
807
|
+
3. **Update** - Use `upsert()` or specific update methods
|
|
808
|
+
4. **Deletion** - Use `delete()` to permanently remove the agent
|
|
809
|
+
|
|
810
|
+
**Important Notes:**
|
|
811
|
+
- Agent IDs are permanent - once created, you cannot change the ID
|
|
812
|
+
- Deleting an agent does not delete associated chat history
|
|
813
|
+
- The built-in agent (accessed via `agent()` with no ID) cannot be deleted
|
|
814
|
+
- **Agent API Keys**: You can pass an options object with an `apiKey` when calling `agent()`:
|
|
815
|
+
- Allows calling agent methods without requiring an App API key
|
|
816
|
+
- Bypasses any `@secureAiAgent` security methods when calling agent ask functions
|
|
817
|
+
- Useful for direct agent access without app-level authentication
|
|
818
|
+
|
|
819
|
+
**Connecting Resources to Agents:**
|
|
820
|
+
|
|
821
|
+
Agents become powerful when connected to resources. You can connect agents to:
|
|
822
|
+
- **Other AI agents** (`connectedAgents`) - Agent collaboration
|
|
823
|
+
- **Backend functions** (`functions`) - Custom logic via `@aiFunction` decorators
|
|
824
|
+
- **Integrations** (`connectedIntegrations`) - Database/API queries
|
|
825
|
+
- **Knowledge bases** (`connectedKnowledgeBases`) - RAG (Retrieval Augmented Generation)
|
|
826
|
+
|
|
827
|
+
**1. Connected Agents (`connectedAgents`):**
|
|
828
|
+
|
|
829
|
+
Allow one agent to call another agent as a specialized sub-task handler.
|
|
830
|
+
|
|
831
|
+
```typescript
|
|
832
|
+
// Define connected agents in upsert or per-request
|
|
833
|
+
const response = await agent.ask('Analyze our sales data and send report to team', {
|
|
834
|
+
connectedAgents: [
|
|
835
|
+
{
|
|
836
|
+
agentId: 'data-analyst-agent',
|
|
837
|
+
description: 'Analyzes sales data and generates reports'
|
|
838
|
+
},
|
|
839
|
+
{
|
|
840
|
+
agentId: 'email-sender-agent',
|
|
841
|
+
description: 'Sends emails to team members'
|
|
842
|
+
}
|
|
843
|
+
]
|
|
844
|
+
});
|
|
845
|
+
|
|
846
|
+
// The main agent can now call these sub-agents when needed
|
|
847
|
+
// Agent orchestration happens automatically based on the prompt
|
|
848
|
+
```
|
|
849
|
+
|
|
850
|
+
**2. Functions (`functions`):**
|
|
851
|
+
|
|
852
|
+
Connect backend functions decorated with `@aiFunction` that the agent can call.
|
|
853
|
+
|
|
854
|
+
```typescript
|
|
855
|
+
// Backend function
|
|
856
|
+
@aiFunction('Gets current inventory for a product', [
|
|
857
|
+
{ name: 'productId', type: 'string', required: true, description: 'Product ID' }
|
|
858
|
+
])
|
|
859
|
+
async getInventory({ productId }: { productId: string }): Promise<number> {
|
|
860
|
+
return await db.inventory.get(productId);
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
// Client - Agent uses the function
|
|
864
|
+
const response = await agent.ask('How many units of product-123 do we have?', {
|
|
865
|
+
functions: ['getInventory'] // Function name or ID
|
|
866
|
+
});
|
|
867
|
+
// Agent automatically calls getInventory() and includes result in response
|
|
868
|
+
|
|
869
|
+
// With predefined parameters (hidden from AI)
|
|
870
|
+
const response = await agent.ask('Send notification', {
|
|
871
|
+
functions: [
|
|
872
|
+
{
|
|
873
|
+
functionId: 'sendEmail',
|
|
874
|
+
context: { apiKey: 'secret-key' } // Passed to function but hidden from AI
|
|
875
|
+
}
|
|
876
|
+
]
|
|
877
|
+
});
|
|
878
|
+
```
|
|
879
|
+
|
|
880
|
+
**3. Connected Integrations (`connectedIntegrations`):**
|
|
881
|
+
|
|
882
|
+
Connect database or API integrations so the agent can query them directly.
|
|
883
|
+
|
|
884
|
+
```typescript
|
|
885
|
+
const response = await agent.ask('Show me active users from last week', {
|
|
886
|
+
connectedIntegrations: [
|
|
887
|
+
{
|
|
888
|
+
integrationId: 'postgres-db',
|
|
889
|
+
integrationType: 'database',
|
|
890
|
+
description: 'Main application database with users, orders, and products',
|
|
891
|
+
instructions: 'Only query the users table unless explicitly asked otherwise',
|
|
892
|
+
options: {
|
|
893
|
+
// Integration-specific options (varies by integration type)
|
|
894
|
+
}
|
|
895
|
+
},
|
|
896
|
+
{
|
|
897
|
+
integrationId: 'stripe-api',
|
|
898
|
+
integrationType: 'api',
|
|
899
|
+
description: 'Stripe payment API for retrieving payment information'
|
|
900
|
+
}
|
|
901
|
+
]
|
|
902
|
+
});
|
|
903
|
+
|
|
904
|
+
// Agent can now query the database and API to answer questions
|
|
905
|
+
```
|
|
906
|
+
|
|
907
|
+
**4. Connected Knowledge Bases (`connectedKnowledgeBases`):**
|
|
908
|
+
|
|
909
|
+
Connect knowledge bases for RAG (Retrieval Augmented Generation) - agent retrieves relevant context before answering.
|
|
910
|
+
|
|
911
|
+
```typescript
|
|
912
|
+
const response = await agent.ask('What is our return policy?', {
|
|
913
|
+
connectedKnowledgeBases: [
|
|
914
|
+
{
|
|
915
|
+
knowledgeBaseId: 'company-policies-kb',
|
|
916
|
+
description: 'Use this when asked about company policies, procedures, or guidelines'
|
|
917
|
+
},
|
|
918
|
+
{
|
|
919
|
+
knowledgeBaseId: 'product-docs-kb',
|
|
920
|
+
description: 'Use this when asked about product features, specifications, or usage'
|
|
921
|
+
}
|
|
922
|
+
]
|
|
923
|
+
});
|
|
924
|
+
|
|
925
|
+
// Agent searches knowledge bases for relevant information before answering
|
|
926
|
+
```
|
|
927
|
+
|
|
928
|
+
**Complete Agent Chat Options:**
|
|
929
|
+
|
|
930
|
+
```typescript
|
|
931
|
+
const agent = squid.ai().agent('my-agent');
|
|
932
|
+
|
|
933
|
+
// Chat (streaming) - Returns RxJS Observable
|
|
934
|
+
// IMPORTANT: Streaming behavior:
|
|
935
|
+
// - NO connected resources: streams token-by-token
|
|
936
|
+
// - HAS connected resources: emits ONCE with complete response
|
|
937
|
+
const chatObs = agent.chat('What is your return policy?', {
|
|
938
|
+
// Memory management
|
|
939
|
+
memoryOptions: {
|
|
940
|
+
memoryMode: 'read-write', // 'none' | 'read-only' | 'read-write'
|
|
941
|
+
memoryId: 'user-123', // Unique per user/session
|
|
942
|
+
expirationMinutes: 1440 // 24 hours
|
|
943
|
+
},
|
|
944
|
+
|
|
945
|
+
// Connected resources (can also be set on agent.upsert)
|
|
946
|
+
connectedAgents: [{ agentId: 'specialist-agent', description: 'Handles X' }],
|
|
947
|
+
functions: ['function1', 'function2'], // or with context: [{ functionId: 'fn', context: {...} }]
|
|
948
|
+
connectedIntegrations: [{ integrationId: 'db', integrationType: 'database', description: '...' }],
|
|
949
|
+
connectedKnowledgeBases: [{ knowledgeBaseId: 'kb1', description: 'When to use this KB' }],
|
|
950
|
+
|
|
951
|
+
// Model & generation
|
|
952
|
+
model: 'gpt-4o', // Override agent's default model
|
|
953
|
+
temperature: 0.7,
|
|
954
|
+
maxTokens: 4000,
|
|
955
|
+
maxOutputTokens: 2000,
|
|
956
|
+
instructions: 'Additional instructions for this request only',
|
|
957
|
+
responseFormat: 'json_object', // or 'text'
|
|
958
|
+
verbosity: 'medium', // 'low' | 'medium' | 'high' (OpenAI only)
|
|
959
|
+
reasoningEffort: 'high', // 'minimal' | 'low' | 'medium' | 'high' (for reasoning models)
|
|
960
|
+
|
|
961
|
+
// Context & RAG
|
|
962
|
+
disableContext: false, // Disable all context
|
|
963
|
+
enablePromptRewriteForRag: false, // Rewrite prompt for better RAG results
|
|
964
|
+
includeReference: false, // Include source references in response
|
|
965
|
+
contextMetadataFilterForKnowledgeBase: {
|
|
966
|
+
'kb-id': { category: 'documentation' } // Filter KB contexts by metadata
|
|
967
|
+
},
|
|
968
|
+
|
|
969
|
+
// Guardrails & quotas
|
|
970
|
+
guardrails: ['no-harmful-content', 'no-pii'], // Preset safety rules
|
|
971
|
+
quotas: {
|
|
972
|
+
maxAiCallStackSize: 5 // Max depth for nested agent calls
|
|
973
|
+
},
|
|
974
|
+
|
|
975
|
+
// Files & voice
|
|
976
|
+
fileUrls: [
|
|
977
|
+
{ id: 'file1', type: 'image', purpose: 'context', url: 'https://...', description: 'Product image' }
|
|
978
|
+
],
|
|
979
|
+
voiceOptions: {
|
|
980
|
+
modelName: 'tts-1',
|
|
981
|
+
voice: 'alloy', // alloy, ash, ballad, coral, echo, fable, onyx, nova, sage, shimmer, verse
|
|
982
|
+
speed: 1.0
|
|
983
|
+
},
|
|
984
|
+
|
|
985
|
+
// Advanced
|
|
986
|
+
smoothTyping: true, // Smooth typing effect (default: true)
|
|
987
|
+
useCodeInterpreter: 'llm', // 'none' | 'llm' (OpenAI/Gemini only)
|
|
988
|
+
executionPlanOptions: {
|
|
989
|
+
enabled: true, // Agent plans which resources to use
|
|
990
|
+
model: 'gpt-4o',
|
|
991
|
+
reasoningEffort: 'high'
|
|
992
|
+
},
|
|
993
|
+
agentContext: { userId: '123', role: 'admin' }, // Global context passed to all functions
|
|
994
|
+
includeMetadata: false // Include context metadata in response
|
|
995
|
+
});
|
|
996
|
+
|
|
997
|
+
chatObs.subscribe(text => {
|
|
998
|
+
console.log(text);
|
|
999
|
+
});
|
|
1000
|
+
|
|
1001
|
+
// Ask (complete response - same options as chat)
|
|
1002
|
+
const response = await agent.ask('Question?', { /* same options */ });
|
|
1003
|
+
|
|
1004
|
+
// Ask with annotations (includes file references, citations)
|
|
1005
|
+
const result = await agent.askWithAnnotations('Question?');
|
|
1006
|
+
console.log(result.responseString);
|
|
1007
|
+
console.log(result.annotations);
|
|
1008
|
+
|
|
1009
|
+
// Voice responses
|
|
1010
|
+
const voiceResult = await agent.askWithVoiceResponse('Hello', {
|
|
1011
|
+
voiceOptions: { modelName: 'tts-1', voice: 'alloy', speed: 1.0 }
|
|
1012
|
+
});
|
|
1013
|
+
console.log(voiceResult.responseString);
|
|
1014
|
+
console.log(voiceResult.voiceResponseFile);
|
|
1015
|
+
|
|
1016
|
+
// Transcribe audio and chat
|
|
1017
|
+
const transcribeResult = await agent.transcribeAndChat(audioFile);
|
|
1018
|
+
console.log(transcribeResult.transcribedPrompt);
|
|
1019
|
+
transcribeResult.responseStream.subscribe(text => console.log(text));
|
|
1020
|
+
|
|
1021
|
+
// Get chat history
|
|
1022
|
+
const history = await agent.getChatHistory('memory-id-123');
|
|
1023
|
+
|
|
1024
|
+
// Semantic search across agent's knowledge
|
|
1025
|
+
const results = await agent.search({ prompt: 'product docs', limit: 10 });
|
|
1026
|
+
|
|
1027
|
+
// Provide feedback on responses
|
|
1028
|
+
await agent.provideFeedback('thumbs_up'); // or 'thumbs_down'
|
|
1029
|
+
|
|
1030
|
+
// Observe status updates (for long-running operations)
|
|
1031
|
+
agent.observeStatusUpdates().subscribe(status => {
|
|
1032
|
+
console.log(status.title, status.tags);
|
|
1033
|
+
});
|
|
1034
|
+
```
|
|
1035
|
+
|
|
1036
|
+
**Agent Security (@secureAiAgent):**
|
|
1037
|
+
|
|
1038
|
+
Secure agent access on the backend:
|
|
1039
|
+
|
|
1040
|
+
**Backend:**
|
|
1041
|
+
```typescript
|
|
1042
|
+
import { SquidService, secureAiAgent } from '@squidcloud/backend';
|
|
1043
|
+
|
|
1044
|
+
export class MyService extends SquidService {
|
|
1045
|
+
// Secure specific agent
|
|
1046
|
+
@secureAiAgent('customer-support-agent')
|
|
1047
|
+
allowCustomerAgent(): boolean {
|
|
1048
|
+
return this.isAuthenticated();
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
// Secure all agents
|
|
1052
|
+
@secureAiAgent()
|
|
1053
|
+
allowAllAgents(): boolean {
|
|
1054
|
+
return this.isAuthenticated();
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
```
|
|
1058
|
+
|
|
1059
|
+
**Important Notes:**
|
|
1060
|
+
- Connected resources can be set on agent creation/update OR per-request
|
|
1061
|
+
- Per-request settings override agent-level settings
|
|
1062
|
+
- Agent automatically decides when to use connected resources based on the prompt
|
|
1063
|
+
- `executionPlanOptions` enables the agent to create a plan before using resources (improves accuracy)
|
|
1064
|
+
- `chatId` and `profileId` are deprecated - use `memoryOptions` instead
|
|
1065
|
+
|
|
1066
|
+
### AI - Knowledge Bases
|
|
1067
|
+
|
|
1068
|
+
```typescript
|
|
1069
|
+
const kb = squid.ai().knowledgeBase('product-docs');
|
|
1070
|
+
|
|
1071
|
+
// Upsert context (with or without file)
|
|
1072
|
+
await kb.upsertContext({
|
|
1073
|
+
contextId: 'doc-123',
|
|
1074
|
+
text: 'Product documentation...',
|
|
1075
|
+
metadata: { category: 'user-guide', version: '2.0' }
|
|
1076
|
+
}, optionalFile);
|
|
1077
|
+
|
|
1078
|
+
// Batch upsert
|
|
1079
|
+
await kb.upsertContexts([
|
|
1080
|
+
{ contextId: 'doc-1', text: '...' },
|
|
1081
|
+
{ contextId: 'doc-2', text: '...' }
|
|
1082
|
+
], optionalFiles);
|
|
1083
|
+
|
|
1084
|
+
// Search with prompt
|
|
1085
|
+
const results = await kb.searchContextsWithPrompt({
|
|
1086
|
+
prompt: 'How do I reset password?',
|
|
1087
|
+
limit: 5,
|
|
1088
|
+
metadata: { category: 'user-guide' }
|
|
1089
|
+
});
|
|
1090
|
+
|
|
1091
|
+
// Search by context ID
|
|
1092
|
+
const related = await kb.searchContextsWithContextId({
|
|
1093
|
+
contextId: 'doc-123',
|
|
1094
|
+
limit: 10
|
|
1095
|
+
});
|
|
1096
|
+
|
|
1097
|
+
// Semantic search (returns chunks)
|
|
1098
|
+
const searchResults = await kb.search({
|
|
1099
|
+
prompt: 'authentication',
|
|
1100
|
+
limit: 10
|
|
1101
|
+
});
|
|
1102
|
+
|
|
1103
|
+
// Get context
|
|
1104
|
+
const context = await kb.getContext('doc-123');
|
|
1105
|
+
|
|
1106
|
+
// List contexts
|
|
1107
|
+
const allContexts = await kb.listContexts(1000); // truncateTextAfter
|
|
1108
|
+
const contextIds = await kb.listContextIds();
|
|
1109
|
+
|
|
1110
|
+
// Download context
|
|
1111
|
+
const download = await kb.downloadContext('doc-123');
|
|
1112
|
+
|
|
1113
|
+
// Delete contexts
|
|
1114
|
+
await kb.deleteContext('doc-123');
|
|
1115
|
+
await kb.deleteContexts(['doc-1', 'doc-2']);
|
|
1116
|
+
|
|
1117
|
+
// List all knowledge bases
|
|
1118
|
+
const allKBs = await squid.ai().listKnowledgeBases();
|
|
1119
|
+
```
|
|
1120
|
+
|
|
1121
|
+
### AI - Image & Audio
|
|
1122
|
+
|
|
1123
|
+
```typescript
|
|
1124
|
+
// Image generation
|
|
1125
|
+
const imageUrl = await squid.ai().image().generate(
|
|
1126
|
+
'A futuristic city',
|
|
1127
|
+
{ size: '1024x1024', quality: 'hd' }
|
|
1128
|
+
);
|
|
1129
|
+
|
|
1130
|
+
// Remove background
|
|
1131
|
+
const noBgBase64 = await squid.ai().image().removeBackground(imageFile);
|
|
1132
|
+
|
|
1133
|
+
// Transcribe audio
|
|
1134
|
+
const text = await squid.ai().audio().transcribe(audioFile, {
|
|
1135
|
+
language: 'en'
|
|
1136
|
+
});
|
|
1137
|
+
|
|
1138
|
+
// Text-to-speech
|
|
1139
|
+
const audioFile = await squid.ai().audio().createSpeech(
|
|
1140
|
+
'Hello world',
|
|
1141
|
+
{ modelName: 'tts-1', voice: 'alloy', speed: 1.0 }
|
|
1142
|
+
);
|
|
1143
|
+
```
|
|
1144
|
+
|
|
1145
|
+
### AI - Query & API Call
|
|
1146
|
+
|
|
1147
|
+
AI Query allows you to query your database using natural language prompts. Squid's AI converts your prompt into database queries and returns the results.
|
|
1148
|
+
|
|
1149
|
+
```typescript
|
|
1150
|
+
// Natural language database query (basic)
|
|
1151
|
+
const result = await squid.ai().executeAiQuery(
|
|
1152
|
+
'built_in_db',
|
|
1153
|
+
'Show me all active users who registered last month'
|
|
1154
|
+
);
|
|
1155
|
+
|
|
1156
|
+
// With options
|
|
1157
|
+
const result = await squid.ai().executeAiQuery(
|
|
1158
|
+
'built_in_db',
|
|
1159
|
+
'Show me all active users who registered last month',
|
|
1160
|
+
{
|
|
1161
|
+
instructions: 'Only query the users collection',
|
|
1162
|
+
generateWalkthrough: true, // Get step-by-step explanation
|
|
1163
|
+
enableRawResults: true, // Include raw query results
|
|
1164
|
+
selectCollectionsOptions: {
|
|
1165
|
+
collectionsToUse: ['users'] // Limit to specific collections
|
|
1166
|
+
},
|
|
1167
|
+
generateQueryOptions: {
|
|
1168
|
+
maxErrorCorrections: 3 // Number of retry attempts
|
|
1169
|
+
},
|
|
1170
|
+
analyzeResultsOptions: {
|
|
1171
|
+
enableCodeInterpreter: true // Enable code execution for result analysis
|
|
1172
|
+
}
|
|
1173
|
+
}
|
|
1174
|
+
);
|
|
1175
|
+
|
|
1176
|
+
// AI-powered API call
|
|
1177
|
+
const apiResult = await squid.ai().executeAiApiCall(
|
|
1178
|
+
'stripe-api',
|
|
1179
|
+
'Create a $50 charge for customer cus_123',
|
|
1180
|
+
['create-charge'], // allowed endpoints
|
|
1181
|
+
true // provide explanation
|
|
1182
|
+
);
|
|
1183
|
+
```
|
|
1184
|
+
|
|
1185
|
+
### Storage
|
|
1186
|
+
|
|
1187
|
+
```typescript
|
|
1188
|
+
const storage = squid.storage(); // built_in_storage
|
|
1189
|
+
const s3 = squid.storage('aws-s3-integration');
|
|
1190
|
+
|
|
1191
|
+
// Upload file
|
|
1192
|
+
await storage.uploadFile('/documents', file, 3600); // 1 hour expiration
|
|
1193
|
+
|
|
1194
|
+
// Get download URL
|
|
1195
|
+
const url = await storage.getDownloadUrl('/documents/report.pdf', 3600);
|
|
1196
|
+
console.log(url.url);
|
|
1197
|
+
|
|
1198
|
+
// Get file metadata
|
|
1199
|
+
const metadata = await storage.getFileMetadata('/documents/report.pdf');
|
|
1200
|
+
console.log(metadata.filename, metadata.size, metadata.lastModified);
|
|
1201
|
+
|
|
1202
|
+
// List directory
|
|
1203
|
+
const contents = await storage.listDirectoryContents('/documents');
|
|
1204
|
+
console.log(contents.files, contents.directories);
|
|
1205
|
+
|
|
1206
|
+
// Delete files
|
|
1207
|
+
await storage.deleteFile('/documents/report.pdf');
|
|
1208
|
+
await storage.deleteFiles(['/docs/file1.pdf', '/docs/file2.pdf']);
|
|
1209
|
+
```
|
|
1210
|
+
|
|
1211
|
+
### Queues
|
|
1212
|
+
|
|
1213
|
+
```typescript
|
|
1214
|
+
const queue = squid.queue<Message>('notifications');
|
|
1215
|
+
const kafkaQueue = squid.queue<Event>('events', 'kafka-integration');
|
|
1216
|
+
|
|
1217
|
+
// Produce messages
|
|
1218
|
+
await queue.produce([
|
|
1219
|
+
{ type: 'email', to: 'user@example.com' },
|
|
1220
|
+
{ type: 'sms', to: '+1234567890' }
|
|
1221
|
+
]);
|
|
1222
|
+
|
|
1223
|
+
// Consume messages (observable)
|
|
1224
|
+
const subscription = queue.consume<Message>().subscribe(message => {
|
|
1225
|
+
console.log('Received:', message);
|
|
1226
|
+
});
|
|
1227
|
+
|
|
1228
|
+
// Unsubscribe
|
|
1229
|
+
subscription.unsubscribe();
|
|
1230
|
+
```
|
|
1231
|
+
|
|
1232
|
+
### Distributed Locks
|
|
1233
|
+
|
|
1234
|
+
```typescript
|
|
1235
|
+
// Acquire and release manually
|
|
1236
|
+
const lock = await squid.acquireLock('payment-processing');
|
|
1237
|
+
try {
|
|
1238
|
+
await processPayment();
|
|
1239
|
+
} finally {
|
|
1240
|
+
lock.release(); // Note: release() is synchronous
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1243
|
+
// Check if released
|
|
1244
|
+
console.log(lock.isReleased()); // true after release
|
|
1245
|
+
|
|
1246
|
+
// Observe release (RxJS Observable)
|
|
1247
|
+
const sub = lock.observeRelease().subscribe(() => {
|
|
1248
|
+
console.log('Lock was released');
|
|
1249
|
+
});
|
|
1250
|
+
|
|
1251
|
+
// Automatic release with withLock (recommended)
|
|
1252
|
+
const result = await squid.withLock('payment-processing', async (lock) => {
|
|
1253
|
+
// Critical section - only one client can execute at a time
|
|
1254
|
+
await processPayment();
|
|
1255
|
+
return 'success';
|
|
1256
|
+
});
|
|
1257
|
+
|
|
1258
|
+
// Lock is automatically released even if callback throws
|
|
1259
|
+
```
|
|
1260
|
+
|
|
1261
|
+
**Important notes:**
|
|
1262
|
+
- Lock is automatically released if WebSocket connection is lost
|
|
1263
|
+
- If lock is already held by another client, `acquireLock()` will reject
|
|
1264
|
+
- Use `@secureDistributedLock(mutexName?)` decorator to secure lock access
|
|
1265
|
+
- Without API key, you must define security rules for locks
|
|
1266
|
+
|
|
1267
|
+
### API Integration
|
|
1268
|
+
|
|
1269
|
+
```typescript
|
|
1270
|
+
const api = squid.api();
|
|
1271
|
+
|
|
1272
|
+
// HTTP methods
|
|
1273
|
+
const customer = await api.get<Customer>('stripe-api', 'get-customer', {
|
|
1274
|
+
pathParams: { customerId: 'cus_123' },
|
|
1275
|
+
queryParams: { expand: 'subscriptions' }
|
|
1276
|
+
});
|
|
1277
|
+
|
|
1278
|
+
const charge = await api.post<Charge, ChargeRequest>(
|
|
1279
|
+
'stripe-api',
|
|
1280
|
+
'create-charge',
|
|
1281
|
+
{ amount: 1000, currency: 'usd' },
|
|
1282
|
+
{ headers: { 'Idempotency-Key': 'unique' } }
|
|
1283
|
+
);
|
|
1284
|
+
|
|
1285
|
+
// Other methods: put, patch, delete
|
|
1286
|
+
await api.put(integrationId, endpointId, body, options);
|
|
1287
|
+
await api.patch(integrationId, endpointId, body, options);
|
|
1288
|
+
await api.delete(integrationId, endpointId, body, options);
|
|
1289
|
+
```
|
|
1290
|
+
|
|
1291
|
+
### Web
|
|
1292
|
+
|
|
1293
|
+
```typescript
|
|
1294
|
+
const web = squid.web();
|
|
1295
|
+
|
|
1296
|
+
// AI-powered web search
|
|
1297
|
+
const results = await web.aiSearch('latest AI developments');
|
|
1298
|
+
console.log(results.markdownContent);
|
|
1299
|
+
console.log(results.citations);
|
|
1300
|
+
|
|
1301
|
+
// Get URL content (as markdown)
|
|
1302
|
+
const content = await web.getUrlContent('https://example.com/article');
|
|
1303
|
+
|
|
1304
|
+
// URL shortening
|
|
1305
|
+
const shortUrl = await web.createShortUrl('https://very-long-url.com', 86400);
|
|
1306
|
+
console.log(shortUrl.url, shortUrl.id);
|
|
1307
|
+
|
|
1308
|
+
const shortUrls = await web.createShortUrls(['url1', 'url2'], 86400);
|
|
1309
|
+
|
|
1310
|
+
await web.deleteShortUrl(shortUrlId);
|
|
1311
|
+
await web.deleteShortUrls([id1, id2]);
|
|
1312
|
+
```
|
|
1313
|
+
|
|
1314
|
+
### Schedulers
|
|
1315
|
+
|
|
1316
|
+
```typescript
|
|
1317
|
+
const schedulers = squid.schedulers;
|
|
1318
|
+
|
|
1319
|
+
// List all
|
|
1320
|
+
const all = await schedulers.list();
|
|
1321
|
+
|
|
1322
|
+
// Enable/disable
|
|
1323
|
+
await schedulers.enable('daily-cleanup');
|
|
1324
|
+
await schedulers.enable(['scheduler-1', 'scheduler-2']);
|
|
1325
|
+
|
|
1326
|
+
await schedulers.disable('hourly-sync');
|
|
1327
|
+
await schedulers.disable(['scheduler-1', 'scheduler-2']);
|
|
1328
|
+
```
|
|
1329
|
+
|
|
1330
|
+
### Observability & Metrics
|
|
1331
|
+
|
|
1332
|
+
```typescript
|
|
1333
|
+
const obs = squid.observability;
|
|
1334
|
+
|
|
1335
|
+
// Report metric
|
|
1336
|
+
await obs.reportMetric({
|
|
1337
|
+
name: 'api_request_count',
|
|
1338
|
+
value: 1,
|
|
1339
|
+
tags: { endpoint: '/users', method: 'GET' },
|
|
1340
|
+
timestamp: Date.now()
|
|
1341
|
+
});
|
|
1342
|
+
|
|
1343
|
+
// Query metrics
|
|
1344
|
+
const metrics = await obs.queryMetrics({
|
|
1345
|
+
metricNames: ['api_request_count'],
|
|
1346
|
+
startTime: startTimestamp,
|
|
1347
|
+
endTime: endTimestamp,
|
|
1348
|
+
groupBy: ['endpoint'],
|
|
1349
|
+
filters: { method: 'GET' },
|
|
1350
|
+
aggregation: 'sum'
|
|
1351
|
+
});
|
|
1352
|
+
|
|
1353
|
+
// Flush pending
|
|
1354
|
+
await obs.flush();
|
|
1355
|
+
```
|
|
1356
|
+
|
|
1357
|
+
### Jobs
|
|
1358
|
+
|
|
1359
|
+
Jobs allow you to track the status of long-running asynchronous operations. Each job has a unique ID that can be used to query its status or wait for completion.
|
|
1360
|
+
|
|
1361
|
+
**Important:** Job IDs must be globally unique and kept private. Anyone who knows a job ID can query its status or result.
|
|
1362
|
+
|
|
1363
|
+
```typescript
|
|
1364
|
+
const jobClient = squid.job();
|
|
1365
|
+
|
|
1366
|
+
// Get job status (returns AsyncJob with status: 'pending' | 'completed' | 'failed')
|
|
1367
|
+
const job = await jobClient.getJob<Result>('job-123');
|
|
1368
|
+
if (job) {
|
|
1369
|
+
console.log('Status:', job.status);
|
|
1370
|
+
if (job.status === 'completed') {
|
|
1371
|
+
console.log('Result:', job.result);
|
|
1372
|
+
} else if (job.status === 'failed') {
|
|
1373
|
+
console.log('Error:', job.error);
|
|
1374
|
+
}
|
|
1375
|
+
}
|
|
1376
|
+
|
|
1377
|
+
// Wait for completion (resolves when job finishes or throws if job fails)
|
|
1378
|
+
const result = await jobClient.awaitJob<Result>('job-123');
|
|
1379
|
+
console.log('Job completed with result:', result);
|
|
1380
|
+
```
|
|
1381
|
+
|
|
1382
|
+
### Admin - Integrations
|
|
1383
|
+
|
|
1384
|
+
**Important:** Admin methods require initialization with an API key. They cannot be used with user authentication.
|
|
1385
|
+
|
|
1386
|
+
```typescript
|
|
1387
|
+
// Squid must be initialized with apiKey
|
|
1388
|
+
const squid = new Squid({ appId: 'app-id', region: 'us-east-1.aws', apiKey: 'your-api-key' });
|
|
1389
|
+
|
|
1390
|
+
const integrations = squid.admin().integrations();
|
|
1391
|
+
|
|
1392
|
+
// List all or by type
|
|
1393
|
+
const all = await integrations.list();
|
|
1394
|
+
const databases = await integrations.list('database');
|
|
1395
|
+
|
|
1396
|
+
// Get one
|
|
1397
|
+
const integration = await integrations.get('postgres-db');
|
|
1398
|
+
|
|
1399
|
+
// Create/update
|
|
1400
|
+
await integrations.upsertIntegration({
|
|
1401
|
+
integrationId: 'my-postgres',
|
|
1402
|
+
type: 'postgres',
|
|
1403
|
+
connectionString: 'postgresql://...'
|
|
1404
|
+
});
|
|
1405
|
+
|
|
1406
|
+
// Delete
|
|
1407
|
+
await integrations.delete('old-integration');
|
|
1408
|
+
await integrations.deleteMany(['int-1', 'int-2']);
|
|
1409
|
+
```
|
|
1410
|
+
|
|
1411
|
+
### Admin - Secrets
|
|
1412
|
+
|
|
1413
|
+
```typescript
|
|
1414
|
+
const secrets = squid.admin().secrets();
|
|
1415
|
+
|
|
1416
|
+
// Get secret
|
|
1417
|
+
const value = await secrets.get('STRIPE_KEY');
|
|
1418
|
+
|
|
1419
|
+
// Get all
|
|
1420
|
+
const all = await secrets.getAll();
|
|
1421
|
+
|
|
1422
|
+
// Create/update
|
|
1423
|
+
await secrets.upsert('API_KEY', 'secret-value');
|
|
1424
|
+
await secrets.upsertMany([
|
|
1425
|
+
{ key: 'KEY1', value: 'val1' },
|
|
1426
|
+
{ key: 'KEY2', value: 'val2' }
|
|
1427
|
+
]);
|
|
1428
|
+
|
|
1429
|
+
// Delete
|
|
1430
|
+
await secrets.delete('OLD_KEY');
|
|
1431
|
+
await secrets.deleteMany(['KEY1', 'KEY2']);
|
|
1432
|
+
|
|
1433
|
+
// API Keys
|
|
1434
|
+
const apiKeys = secrets.apiKeys;
|
|
1435
|
+
await apiKeys.upsert('my-key');
|
|
1436
|
+
const key = await apiKeys.get('my-key');
|
|
1437
|
+
await apiKeys.delete('my-key');
|
|
1438
|
+
```
|
|
1439
|
+
|
|
1440
|
+
## Backend SDK
|
|
1441
|
+
|
|
1442
|
+
### SquidService Base Class
|
|
1443
|
+
|
|
1444
|
+
All backend services extend `SquidService`:
|
|
1445
|
+
|
|
1446
|
+
```typescript
|
|
1447
|
+
import { SquidService } from '@squidcloud/backend';
|
|
1448
|
+
|
|
1449
|
+
export class MyService extends SquidService {
|
|
1450
|
+
// Decorated methods
|
|
1451
|
+
}
|
|
1452
|
+
```
|
|
1453
|
+
|
|
1454
|
+
**Important:** In backend code running on Squid's infrastructure, the Squid client is **already initialized and available** via `this.squid`. You don't need to manually create a new instance or provide configuration parameters - simply use the pre-configured client.
|
|
1455
|
+
|
|
1456
|
+
**Available properties:**
|
|
1457
|
+
|
|
1458
|
+
```typescript
|
|
1459
|
+
this.region // 'local' during dev, region in production
|
|
1460
|
+
this.backendBaseUrl
|
|
1461
|
+
this.secrets // From Squid Console
|
|
1462
|
+
this.apiKeys
|
|
1463
|
+
this.context // RunContext (appId, clientId, sourceIp, headers, openApiContext)
|
|
1464
|
+
this.squid // Pre-initialized Squid client instance (ready to use)
|
|
1465
|
+
this.assetsDirectory // Path to public/ folder
|
|
1466
|
+
```
|
|
1467
|
+
|
|
1468
|
+
**Auth methods:**
|
|
1469
|
+
|
|
1470
|
+
```typescript
|
|
1471
|
+
// Get user authentication (JWT token)
|
|
1472
|
+
this.getUserAuth() // AuthWithBearer | undefined
|
|
1473
|
+
// Returns: { type: 'Bearer', userId: string, expiration: number, attributes: Record<string, any>, jwt?: string }
|
|
1474
|
+
|
|
1475
|
+
// Get API key authentication
|
|
1476
|
+
this.getApiKeyAuth() // AuthWithApiKey | undefined
|
|
1477
|
+
// Returns: { type: 'ApiKey', apiKey: string }
|
|
1478
|
+
|
|
1479
|
+
this.isAuthenticated() // boolean - true if user token OR API key
|
|
1480
|
+
this.assertIsAuthenticated() // throws if not authenticated
|
|
1481
|
+
this.assertApiKeyCall() // throws if not API key auth
|
|
1482
|
+
```
|
|
1483
|
+
|
|
1484
|
+
**Helper methods:**
|
|
1485
|
+
|
|
1486
|
+
```typescript
|
|
1487
|
+
// Create webhook response (for @webhook decorated functions)
|
|
1488
|
+
this.createWebhookResponse(body?, statusCode?, headers?)
|
|
1489
|
+
// Throws webhook response immediately (interrupts execution)
|
|
1490
|
+
this.throwWebhookResponse({ body?, statusCode?, headers? })
|
|
1491
|
+
|
|
1492
|
+
// Create OpenAPI response (for OpenAPI/tsoa decorated functions)
|
|
1493
|
+
this.createOpenApiResponse(body?, statusCode?, headers?)
|
|
1494
|
+
// Throws OpenAPI response immediately (interrupts execution)
|
|
1495
|
+
this.throwOpenApiResponse({ body?, statusCode?, headers? })
|
|
1496
|
+
|
|
1497
|
+
// Convert browser File to SquidFile for OpenAPI file returns
|
|
1498
|
+
await this.convertToSquidFile(file: File): Promise<SquidFile>
|
|
1499
|
+
|
|
1500
|
+
// Publish AI status update to specific client (used in @aiFunction)
|
|
1501
|
+
await this.publishAiStatusUpdate(update: AiStatusMessage, clientId: ClientId)
|
|
1502
|
+
```
|
|
1503
|
+
|
|
1504
|
+
### Backend Decorators
|
|
1505
|
+
|
|
1506
|
+
This section contains backend decorators that don't have corresponding client methods (triggers, schedulers, security rules, etc.).
|
|
1507
|
+
|
|
1508
|
+
#### @trigger(options)
|
|
1509
|
+
Responds to database collection changes.
|
|
1510
|
+
|
|
1511
|
+
```typescript
|
|
1512
|
+
@trigger({ id: 'user-created', collection: 'users', mutationTypes: ['insert'] })
|
|
1513
|
+
async onUserCreated(request: TriggerRequest<User>): Promise<void> {
|
|
1514
|
+
console.log(request.mutationType); // 'insert' | 'update' | 'delete'
|
|
1515
|
+
console.log(request.docBefore);
|
|
1516
|
+
console.log(request.docAfter);
|
|
1517
|
+
}
|
|
1518
|
+
```
|
|
1519
|
+
|
|
1520
|
+
#### @scheduler(options)
|
|
1521
|
+
Schedules periodic execution using cron expressions.
|
|
1522
|
+
|
|
1523
|
+
```typescript
|
|
1524
|
+
import { CronExpression } from '@squidcloud/backend';
|
|
1525
|
+
|
|
1526
|
+
// Using cron string directly
|
|
1527
|
+
@scheduler({ id: 'daily-cleanup', cron: '0 0 * * *' }) // Daily at midnight UTC
|
|
1528
|
+
async cleanup(): Promise<void> {
|
|
1529
|
+
console.log('Running cleanup');
|
|
1530
|
+
}
|
|
1531
|
+
|
|
1532
|
+
// Using predefined CronExpression enum
|
|
1533
|
+
@scheduler({ id: 'hourly-sync', cron: CronExpression.EVERY_HOUR })
|
|
1534
|
+
async hourlySync(): Promise<void> {
|
|
1535
|
+
console.log('Running hourly sync');
|
|
1536
|
+
}
|
|
1537
|
+
|
|
1538
|
+
@scheduler({ id: 'weekday-morning', cron: CronExpression.MONDAY_TO_FRIDAY_AT_9AM })
|
|
1539
|
+
async weekdayTask(): Promise<void> {
|
|
1540
|
+
console.log('Running weekday morning task');
|
|
1541
|
+
}
|
|
1542
|
+
|
|
1543
|
+
@scheduler({ id: 'frequent', cron: CronExpression.EVERY_5_MINUTES, exclusive: false })
|
|
1544
|
+
async frequentTask(): Promise<void> {
|
|
1545
|
+
// exclusive: false allows concurrent runs
|
|
1546
|
+
}
|
|
1547
|
+
```
|
|
1548
|
+
|
|
1549
|
+
**Common CronExpression values:**
|
|
1550
|
+
- `EVERY_SECOND`, `EVERY_5_SECONDS`, `EVERY_10_SECONDS`, `EVERY_30_SECONDS`
|
|
1551
|
+
- `EVERY_MINUTE`, `EVERY_5_MINUTES`, `EVERY_10_MINUTES`, `EVERY_30_MINUTES`
|
|
1552
|
+
- `EVERY_HOUR`, `EVERY_2_HOURS`, `EVERY_3_HOURS`, etc.
|
|
1553
|
+
- `EVERY_DAY_AT_MIDNIGHT`, `EVERY_DAY_AT_NOON`, `EVERY_DAY_AT_1AM`, etc.
|
|
1554
|
+
- `EVERY_WEEKDAY`, `EVERY_WEEKEND`, `EVERY_WEEK`
|
|
1555
|
+
- `MONDAY_TO_FRIDAY_AT_9AM`, `MONDAY_TO_FRIDAY_AT_5PM`, etc.
|
|
1556
|
+
- `EVERY_1ST_DAY_OF_MONTH_AT_MIDNIGHT`, `EVERY_QUARTER`, `EVERY_YEAR`
|
|
1557
|
+
|
|
1558
|
+
#### @secureTopic(topicName, type, integrationId?)
|
|
1559
|
+
Secures queue topics.
|
|
1560
|
+
|
|
1561
|
+
```typescript
|
|
1562
|
+
@secureTopic('notifications', 'read')
|
|
1563
|
+
allowRead(): boolean {
|
|
1564
|
+
return this.isAuthenticated();
|
|
1565
|
+
}
|
|
1566
|
+
|
|
1567
|
+
@secureTopic('notifications', 'write')
|
|
1568
|
+
allowWrite(context: TopicWriteContext<any>): boolean {
|
|
1569
|
+
return context.messages.length <= 100;
|
|
1570
|
+
}
|
|
1571
|
+
```
|
|
1572
|
+
|
|
1573
|
+
Types: `'read'`, `'write'`, `'all'`
|
|
1574
|
+
|
|
1575
|
+
#### @secureStorage(type, integrationId?)
|
|
1576
|
+
Secures storage operations.
|
|
1577
|
+
|
|
1578
|
+
```typescript
|
|
1579
|
+
@secureStorage('write')
|
|
1580
|
+
allowWrite(context: StorageContext): boolean {
|
|
1581
|
+
return !context.pathsInBucket.some(p => p.startsWith('/admin'));
|
|
1582
|
+
}
|
|
1583
|
+
```
|
|
1584
|
+
|
|
1585
|
+
Types: `'read'`, `'write'`, `'update'`, `'insert'`, `'delete'`, `'all'`
|
|
1586
|
+
|
|
1587
|
+
#### @secureApi(integrationId, endpointId?)
|
|
1588
|
+
Secures API integrations.
|
|
1589
|
+
|
|
1590
|
+
```typescript
|
|
1591
|
+
@secureApi('stripe-api', 'create-charge')
|
|
1592
|
+
allowCharge(context: ApiCallContext): boolean {
|
|
1593
|
+
const amount = (context.body as any)?.amount;
|
|
1594
|
+
return amount < 10000;
|
|
1595
|
+
}
|
|
1596
|
+
|
|
1597
|
+
@secureApi('external-api') // Secures entire API
|
|
1598
|
+
allowApi(): boolean {
|
|
1599
|
+
return this.isAuthenticated();
|
|
1600
|
+
}
|
|
1601
|
+
```
|
|
1602
|
+
|
|
1603
|
+
#### @secureNativeQuery(integrationId)
|
|
1604
|
+
Secures native database queries.
|
|
1605
|
+
|
|
1606
|
+
```typescript
|
|
1607
|
+
@secureNativeQuery('postgres-db')
|
|
1608
|
+
allowQuery(context: NativeQueryContext): boolean {
|
|
1609
|
+
if (context.type === 'relational') {
|
|
1610
|
+
return !context.query.toUpperCase().includes('DROP');
|
|
1611
|
+
}
|
|
1612
|
+
return true;
|
|
1613
|
+
}
|
|
1614
|
+
```
|
|
1615
|
+
|
|
1616
|
+
#### @secureAiQuery(integrationId?)
|
|
1617
|
+
Secures AI query execution.
|
|
1618
|
+
|
|
1619
|
+
```typescript
|
|
1620
|
+
@secureAiQuery()
|
|
1621
|
+
allowAiQuery(context: AiQueryContext): boolean {
|
|
1622
|
+
return this.isAuthenticated() && context.prompt.length < 1000;
|
|
1623
|
+
}
|
|
1624
|
+
```
|
|
1625
|
+
|
|
1626
|
+
#### @secureAiAgent(agentId?)
|
|
1627
|
+
Secures AI agent access.
|
|
1628
|
+
|
|
1629
|
+
```typescript
|
|
1630
|
+
@secureAiAgent('customer-bot')
|
|
1631
|
+
allowAgent(context: SecureAiAgentContext): boolean {
|
|
1632
|
+
return !context.prompt?.includes('admin');
|
|
1633
|
+
}
|
|
1634
|
+
|
|
1635
|
+
@secureAiAgent() // All agents
|
|
1636
|
+
allowAllAgents(): boolean {
|
|
1637
|
+
return this.isAuthenticated();
|
|
1638
|
+
}
|
|
1639
|
+
```
|
|
1640
|
+
|
|
1641
|
+
#### @secureDistributedLock(mutexName?)
|
|
1642
|
+
Secures distributed lock access.
|
|
1643
|
+
|
|
1644
|
+
```typescript
|
|
1645
|
+
// Secure specific mutex
|
|
1646
|
+
@secureDistributedLock('payment-processing')
|
|
1647
|
+
allowPaymentLock(context: DistributedLockContext): boolean {
|
|
1648
|
+
// context.mutex contains the mutex name
|
|
1649
|
+
return this.isAuthenticated() && context.mutex === 'payment-processing';
|
|
1650
|
+
}
|
|
1651
|
+
|
|
1652
|
+
// Secure all mutexes
|
|
1653
|
+
@secureDistributedLock()
|
|
1654
|
+
allowAllLocks(context: DistributedLockContext): boolean {
|
|
1655
|
+
return this.isAuthenticated();
|
|
1656
|
+
}
|
|
1657
|
+
```
|
|
1658
|
+
|
|
1659
|
+
**Important:** Without API key, you must define `@secureDistributedLock` decorators to allow lock acquisition.
|
|
1660
|
+
|
|
1661
|
+
#### @aiFunction(options)
|
|
1662
|
+
Exposes function to AI agents. Agents automatically call these functions based on user prompts.
|
|
1663
|
+
|
|
1664
|
+
```typescript
|
|
1665
|
+
// Simple form: description and params array
|
|
1666
|
+
@aiFunction('Returns the names of the pirates crew on a given ship name', [
|
|
1667
|
+
{ name: 'shipName', type: 'string', required: true, description: 'The name of the ship' }
|
|
1668
|
+
])
|
|
1669
|
+
async getShipCrew({ shipName }: { shipName: string }): Promise<string[]> {
|
|
1670
|
+
const crew = await getCrew(shipName);
|
|
1671
|
+
return crew.map(m => m.name);
|
|
1672
|
+
}
|
|
1673
|
+
|
|
1674
|
+
// Options object form with more control
|
|
1675
|
+
@aiFunction({
|
|
1676
|
+
id: 'custom-function-id', // Optional custom ID
|
|
1677
|
+
description: 'Get user profile',
|
|
1678
|
+
params: [
|
|
1679
|
+
{ name: 'userId', type: 'string', required: true, description: 'User ID' }
|
|
1680
|
+
],
|
|
1681
|
+
attributes: {
|
|
1682
|
+
integrationType: ['salesforce'] // Only available when specific integrations connected
|
|
1683
|
+
}
|
|
1684
|
+
})
|
|
1685
|
+
async getUserProfile(
|
|
1686
|
+
params: { userId: string },
|
|
1687
|
+
context: AiFunctionCallContext
|
|
1688
|
+
): Promise<User> {
|
|
1689
|
+
console.log('Agent ID:', context.agentId);
|
|
1690
|
+
console.log('Integration ID:', context.integrationId);
|
|
1691
|
+
return { userId: params.userId, name: 'John' };
|
|
1692
|
+
}
|
|
1693
|
+
|
|
1694
|
+
// Parameter types: 'string' | 'number' | 'boolean' | 'date' | 'files'
|
|
1695
|
+
@aiFunction('Books a hotel room', [
|
|
1696
|
+
{ name: 'hotelName', type: 'string', required: true, description: 'Name of hotel' },
|
|
1697
|
+
{ name: 'checkInDate', type: 'date', required: true, description: 'Check-in date' },
|
|
1698
|
+
{ name: 'numberOfGuests', type: 'number', required: true, description: 'Number of guests' },
|
|
1699
|
+
{ name: 'breakfast', type: 'boolean', required: false, description: 'Include breakfast' },
|
|
1700
|
+
{ name: 'roomType', type: 'string', required: true, description: 'Type of room',
|
|
1701
|
+
enum: ['single', 'double', 'suite'] }
|
|
1702
|
+
])
|
|
1703
|
+
async bookHotel(args: BookingArgs): Promise<string> {
|
|
1704
|
+
return `Booked ${args.roomType} room at ${args.hotelName}`;
|
|
1705
|
+
}
|
|
1706
|
+
|
|
1707
|
+
// Using predefined parameters (hidden from AI)
|
|
1708
|
+
// Call from client with: agent.ask(prompt, {
|
|
1709
|
+
// functions: ['sendEmail'],
|
|
1710
|
+
// predefinedParams: { sendEmail: { apiKey: 'secret' } }
|
|
1711
|
+
// })
|
|
1712
|
+
@aiFunction('Sends an email', [
|
|
1713
|
+
{ name: 'recipient', type: 'string', required: true, description: 'Email recipient' },
|
|
1714
|
+
{ name: 'subject', type: 'string', required: true, description: 'Email subject' },
|
|
1715
|
+
{ name: 'body', type: 'string', required: true, description: 'Email body' }
|
|
1716
|
+
])
|
|
1717
|
+
async sendEmail(args: { recipient: string; subject: string; body: string; apiKey?: string }): Promise<string> {
|
|
1718
|
+
await emailService.send(args.apiKey, args.recipient, args.subject, args.body);
|
|
1719
|
+
return 'Email sent successfully';
|
|
1720
|
+
}
|
|
1721
|
+
```
|
|
1722
|
+
|
|
1723
|
+
**Using functions from client:**
|
|
1724
|
+
```typescript
|
|
1725
|
+
const response = await agent.ask('What is the crew of the Black Pearl?', {
|
|
1726
|
+
functions: ['getShipCrew', 'getShipDetails'] // Function names or custom IDs
|
|
1727
|
+
});
|
|
1728
|
+
```
|
|
1729
|
+
|
|
1730
|
+
#### @limits(options)
|
|
1731
|
+
Rate/quota limiting.
|
|
1732
|
+
|
|
1733
|
+
```typescript
|
|
1734
|
+
// Simple rate limit (5 QPS globally)
|
|
1735
|
+
@limits({ rateLimit: 5 })
|
|
1736
|
+
async limited(): Promise<void> {}
|
|
1737
|
+
|
|
1738
|
+
// Quota (100 calls/month per user)
|
|
1739
|
+
@limits({ quotaLimit: { value: 100, scope: 'user', renewPeriod: 'monthly' } })
|
|
1740
|
+
async quotaLimited(): Promise<void> {}
|
|
1741
|
+
|
|
1742
|
+
// Multiple limits
|
|
1743
|
+
@limits({
|
|
1744
|
+
rateLimit: [
|
|
1745
|
+
{ value: 100, scope: 'global' },
|
|
1746
|
+
{ value: 10, scope: 'user' }
|
|
1747
|
+
],
|
|
1748
|
+
quotaLimit: [
|
|
1749
|
+
{ value: 10000, scope: 'global', renewPeriod: 'monthly' },
|
|
1750
|
+
{ value: 500, scope: 'user', renewPeriod: 'weekly' }
|
|
1751
|
+
]
|
|
1752
|
+
})
|
|
1753
|
+
async multiLimited(): Promise<void> {}
|
|
1754
|
+
```
|
|
1755
|
+
|
|
1756
|
+
Scopes: `'global'`, `'user'`, `'ip'`
|
|
1757
|
+
Periods: `'hourly'`, `'daily'`, `'weekly'`, `'monthly'`, `'quarterly'`, `'annually'`
|
|
1758
|
+
|
|
1759
|
+
## Common Patterns
|
|
1760
|
+
|
|
1761
|
+
### Authentication
|
|
1762
|
+
|
|
1763
|
+
Client:
|
|
1764
|
+
```typescript
|
|
1765
|
+
const squid = new Squid({
|
|
1766
|
+
appId: 'YOUR_APP_ID',
|
|
1767
|
+
region: 'us-east-1.aws',
|
|
1768
|
+
authProvider: {
|
|
1769
|
+
integrationId: 'auth0',
|
|
1770
|
+
getToken: async () => await auth0.getAccessTokenSilently()
|
|
1771
|
+
}
|
|
1772
|
+
});
|
|
1773
|
+
```
|
|
1774
|
+
|
|
1775
|
+
Backend:
|
|
1776
|
+
|
|
1777
|
+
```typescript
|
|
1778
|
+
@executable()
|
|
1779
|
+
async getUserData(): Promise<UserData> {
|
|
1780
|
+
this.assertIsAuthenticated();
|
|
1781
|
+
const userId = this.getUserAuth()?.userId;
|
|
1782
|
+
if (!userId) throw new Error('User not authenticated');
|
|
1783
|
+
return await fetchUserData(userId);
|
|
1784
|
+
}
|
|
1785
|
+
|
|
1786
|
+
@secureCollection('users', 'read')
|
|
1787
|
+
allowRead(context: QueryContext): boolean {
|
|
1788
|
+
const userId = this.getUserAuth()?.userId;
|
|
1789
|
+
if (!userId) return false;
|
|
1790
|
+
return context.isSubqueryOf('id', '==', userId);
|
|
1791
|
+
}
|
|
1792
|
+
```
|
|
1793
|
+
|
|
1794
|
+
### File Upload
|
|
1795
|
+
|
|
1796
|
+
Client:
|
|
1797
|
+
```typescript
|
|
1798
|
+
const file: File = ...; // from input
|
|
1799
|
+
await squid.storage().uploadFile('/uploads', file);
|
|
1800
|
+
// OR
|
|
1801
|
+
await squid.executeFunction('processFile', file);
|
|
1802
|
+
```
|
|
1803
|
+
|
|
1804
|
+
Backend:
|
|
1805
|
+
```typescript
|
|
1806
|
+
@executable()
|
|
1807
|
+
async processFile(file: SquidFile): Promise<Result> {
|
|
1808
|
+
console.log(file.originalName, file.size, file.mimetype);
|
|
1809
|
+
const content = new TextDecoder().decode(file.data);
|
|
1810
|
+
return { processed: true };
|
|
1811
|
+
}
|
|
1812
|
+
```
|
|
1813
|
+
|
|
1814
|
+
### Using Squid Client in Backend
|
|
1815
|
+
|
|
1816
|
+
```typescript
|
|
1817
|
+
export class MyService extends SquidService {
|
|
1818
|
+
@executable()
|
|
1819
|
+
async aggregateStats(userId: string): Promise<Stats> {
|
|
1820
|
+
const orders = await this.squid.collection('orders')
|
|
1821
|
+
.query()
|
|
1822
|
+
.eq('userId', userId)
|
|
1823
|
+
.snapshot();
|
|
1824
|
+
|
|
1825
|
+
return { totalOrders: orders.data.length };
|
|
1826
|
+
}
|
|
1827
|
+
}
|
|
1828
|
+
```
|
|
1829
|
+
|
|
1830
|
+
## Best Practices
|
|
1831
|
+
|
|
1832
|
+
1. **Always secure collections/topics/storage** - Default deny
|
|
1833
|
+
2. **Validate input in executables** - Check auth and params
|
|
1834
|
+
3. **Use transactions for multi-doc updates** - Atomic operations
|
|
1835
|
+
4. **Limit query results** - Default 1000, max 20000
|
|
1836
|
+
5. **Use snapshots for one-time data** - Use subscriptions for realtime
|
|
1837
|
+
6. **Batch operations when possible** - insertMany, deleteMany
|
|
1838
|
+
7. **Use memoryOptions for AI conversations** - Not deprecated chatId
|
|
1839
|
+
8. **Test in dev before prod deployment** - `squid deploy --environmentId dev`
|
|
1840
|
+
|
|
1841
|
+
## Documentation
|
|
1842
|
+
|
|
1843
|
+
- Docs: https://docs.getsquid.ai/
|
|
1844
|
+
- Console: https://console.getsquid.ai/
|
|
1845
|
+
- Samples: https://github.com/squid-cloud-samples
|
|
1846
|
+
|
|
1847
|
+
---
|
|
1848
|
+
|
|
1849
|
+
**This skill is verified against the actual Squid source code and contains only accurate, tested APIs.**
|