openkbs 0.0.64 → 0.0.65
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/README.md +2 -0
- package/elastic/README.md +1150 -0
- package/elastic/functions.md +328 -0
- package/elastic/postgres.md +287 -0
- package/elastic/pulse.md +386 -0
- package/elastic/storage.md +291 -0
- package/package.json +1 -1
- package/src/actions.js +35 -7
- package/src/index.js +15 -1
- package/src/utils.js +8 -4
- package/version.json +3 -3
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
# Tutorial 13: Serverless Functions
|
|
2
|
+
|
|
3
|
+
Deploy serverless APIs with AWS Lambda. Write your code in Node.js, Python, or Java. Get HTTPS endpoints automatically.
|
|
4
|
+
|
|
5
|
+
## Create a Function
|
|
6
|
+
|
|
7
|
+
### Project Structure
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
my-app/
|
|
11
|
+
├── openkbs.json
|
|
12
|
+
├── functions/
|
|
13
|
+
│ └── api/
|
|
14
|
+
│ ├── index.mjs # Node.js handler
|
|
15
|
+
│ └── package.json # Dependencies
|
|
16
|
+
└── site/
|
|
17
|
+
└── index.html
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
### Node.js Function
|
|
21
|
+
|
|
22
|
+
Create `functions/api/index.mjs`:
|
|
23
|
+
|
|
24
|
+
```javascript
|
|
25
|
+
export async function handler(event) {
|
|
26
|
+
const { action, ...data } = JSON.parse(event.body || '{}');
|
|
27
|
+
|
|
28
|
+
switch (action) {
|
|
29
|
+
case 'hello':
|
|
30
|
+
return {
|
|
31
|
+
statusCode: 200,
|
|
32
|
+
headers: { 'Content-Type': 'application/json' },
|
|
33
|
+
body: JSON.stringify({ message: `Hello, ${data.name || 'World'}!` })
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
case 'echo':
|
|
37
|
+
return {
|
|
38
|
+
statusCode: 200,
|
|
39
|
+
body: JSON.stringify({ received: data })
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
default:
|
|
43
|
+
return {
|
|
44
|
+
statusCode: 400,
|
|
45
|
+
body: JSON.stringify({ error: 'Unknown action' })
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Create `functions/api/package.json`:
|
|
52
|
+
|
|
53
|
+
```json
|
|
54
|
+
{
|
|
55
|
+
"type": "module",
|
|
56
|
+
"dependencies": {}
|
|
57
|
+
}
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### Python Function
|
|
61
|
+
|
|
62
|
+
Create `functions/api/handler.py`:
|
|
63
|
+
|
|
64
|
+
```python
|
|
65
|
+
import json
|
|
66
|
+
|
|
67
|
+
def handler(event, context):
|
|
68
|
+
body = json.loads(event.get('body', '{}'))
|
|
69
|
+
action = body.get('action')
|
|
70
|
+
|
|
71
|
+
if action == 'hello':
|
|
72
|
+
name = body.get('name', 'World')
|
|
73
|
+
return {
|
|
74
|
+
'statusCode': 200,
|
|
75
|
+
'body': json.dumps({'message': f'Hello, {name}!'})
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return {
|
|
79
|
+
'statusCode': 400,
|
|
80
|
+
'body': json.dumps({'error': 'Unknown action'})
|
|
81
|
+
}
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Create `functions/api/requirements.txt`:
|
|
85
|
+
|
|
86
|
+
```
|
|
87
|
+
# Add Python dependencies here
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### Java Function
|
|
91
|
+
|
|
92
|
+
Create `functions/api/src/main/java/com/example/Handler.java`:
|
|
93
|
+
|
|
94
|
+
```java
|
|
95
|
+
package com.example;
|
|
96
|
+
|
|
97
|
+
import com.amazonaws.services.lambda.runtime.Context;
|
|
98
|
+
import com.amazonaws.services.lambda.runtime.RequestHandler;
|
|
99
|
+
import com.google.gson.Gson;
|
|
100
|
+
import java.util.Map;
|
|
101
|
+
|
|
102
|
+
public class Handler implements RequestHandler<Map<String, Object>, Map<String, Object>> {
|
|
103
|
+
private static final Gson gson = new Gson();
|
|
104
|
+
|
|
105
|
+
@Override
|
|
106
|
+
public Map<String, Object> handleRequest(Map<String, Object> event, Context context) {
|
|
107
|
+
String body = (String) event.get("body");
|
|
108
|
+
Map<String, Object> request = gson.fromJson(body, Map.class);
|
|
109
|
+
String action = (String) request.get("action");
|
|
110
|
+
|
|
111
|
+
if ("hello".equals(action)) {
|
|
112
|
+
String name = (String) request.getOrDefault("name", "World");
|
|
113
|
+
return Map.of(
|
|
114
|
+
"statusCode", 200,
|
|
115
|
+
"body", gson.toJson(Map.of("message", "Hello, " + name + "!"))
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return Map.of("statusCode", 400, "body", "{\"error\":\"Unknown action\"}");
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
## Deploy
|
|
125
|
+
|
|
126
|
+
```bash
|
|
127
|
+
# Install dependencies
|
|
128
|
+
cd functions/api && npm install && cd ../..
|
|
129
|
+
|
|
130
|
+
# Deploy function
|
|
131
|
+
openkbs fn push api
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
Your function is live at `https://your-kb.openkbs.com/api`.
|
|
135
|
+
|
|
136
|
+
## Test Your Function
|
|
137
|
+
|
|
138
|
+
### From CLI
|
|
139
|
+
|
|
140
|
+
```bash
|
|
141
|
+
openkbs fn invoke api '{"action":"hello","name":"OpenKBS"}'
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
### From Browser
|
|
145
|
+
|
|
146
|
+
```javascript
|
|
147
|
+
const response = await fetch('/api', {
|
|
148
|
+
method: 'POST',
|
|
149
|
+
headers: { 'Content-Type': 'application/json' },
|
|
150
|
+
body: JSON.stringify({ action: 'hello', name: 'OpenKBS' })
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
const data = await response.json();
|
|
154
|
+
console.log(data); // { message: "Hello, OpenKBS!" }
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
### With curl
|
|
158
|
+
|
|
159
|
+
```bash
|
|
160
|
+
curl -X POST https://your-kb.openkbs.com/api \
|
|
161
|
+
-H "Content-Type: application/json" \
|
|
162
|
+
-d '{"action":"hello","name":"OpenKBS"}'
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
## Environment Variables
|
|
166
|
+
|
|
167
|
+
Elastic services inject these automatically:
|
|
168
|
+
|
|
169
|
+
| Variable | Description |
|
|
170
|
+
|----------|-------------|
|
|
171
|
+
| `OPENKBS_KB_ID` | Your KB ID |
|
|
172
|
+
| `OPENKBS_API_KEY` | API key |
|
|
173
|
+
| `DATABASE_URL` | PostgreSQL connection (if enabled) |
|
|
174
|
+
| `STORAGE_BUCKET` | S3 bucket name (if enabled) |
|
|
175
|
+
| `STORAGE_REGION` | S3 region (if enabled) |
|
|
176
|
+
|
|
177
|
+
### View Environment
|
|
178
|
+
|
|
179
|
+
```bash
|
|
180
|
+
openkbs fn env api
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
### Set Custom Variables
|
|
184
|
+
|
|
185
|
+
```bash
|
|
186
|
+
openkbs fn env api STRIPE_KEY=sk_live_xxx
|
|
187
|
+
openkbs fn env api DEBUG=true
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
### Remove Variable
|
|
191
|
+
|
|
192
|
+
```bash
|
|
193
|
+
openkbs fn env api STRIPE_KEY=
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
## Configure Memory & Timeout
|
|
197
|
+
|
|
198
|
+
```bash
|
|
199
|
+
# Default: 128MB memory, 30s timeout
|
|
200
|
+
openkbs fn push api
|
|
201
|
+
|
|
202
|
+
# More memory (= more CPU)
|
|
203
|
+
openkbs fn push api --memory 512
|
|
204
|
+
|
|
205
|
+
# Longer timeout (max 900s)
|
|
206
|
+
openkbs fn push api --timeout 60
|
|
207
|
+
|
|
208
|
+
# Both
|
|
209
|
+
openkbs fn push api --memory 1024 --timeout 120
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
Memory options: 128, 256, 512, 1024, 2048, 3008 MB
|
|
213
|
+
|
|
214
|
+
## View Logs
|
|
215
|
+
|
|
216
|
+
```bash
|
|
217
|
+
openkbs fn logs api
|
|
218
|
+
openkbs fn logs api --limit 100
|
|
219
|
+
openkbs fn logs api --follow # Stream logs
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
## CORS Headers
|
|
223
|
+
|
|
224
|
+
For browser access, add CORS headers:
|
|
225
|
+
|
|
226
|
+
```javascript
|
|
227
|
+
export async function handler(event) {
|
|
228
|
+
const headers = {
|
|
229
|
+
'Content-Type': 'application/json',
|
|
230
|
+
'Access-Control-Allow-Origin': '*',
|
|
231
|
+
'Access-Control-Allow-Headers': 'Content-Type'
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
// Handle OPTIONS preflight
|
|
235
|
+
if (event.requestContext?.http?.method === 'OPTIONS') {
|
|
236
|
+
return { statusCode: 200, headers, body: '' };
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Your logic here...
|
|
240
|
+
|
|
241
|
+
return { statusCode: 200, headers, body: JSON.stringify({ success: true }) };
|
|
242
|
+
}
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
## Multiple Functions
|
|
246
|
+
|
|
247
|
+
```
|
|
248
|
+
functions/
|
|
249
|
+
├── auth/
|
|
250
|
+
│ ├── index.mjs
|
|
251
|
+
│ └── package.json
|
|
252
|
+
├── posts/
|
|
253
|
+
│ ├── index.mjs
|
|
254
|
+
│ └── package.json
|
|
255
|
+
└── payments/
|
|
256
|
+
├── index.mjs
|
|
257
|
+
└── package.json
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
`openkbs.json`:
|
|
261
|
+
```json
|
|
262
|
+
{
|
|
263
|
+
"name": "my-app",
|
|
264
|
+
"elastic": { "postgres": true },
|
|
265
|
+
"functions": ["auth", "posts", "payments"]
|
|
266
|
+
}
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
Deploy all at once:
|
|
270
|
+
```bash
|
|
271
|
+
openkbs deploy
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
Or individually:
|
|
275
|
+
```bash
|
|
276
|
+
openkbs fn push auth
|
|
277
|
+
openkbs fn push posts
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
## List Functions
|
|
281
|
+
|
|
282
|
+
```bash
|
|
283
|
+
openkbs fn list
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
Output:
|
|
287
|
+
```
|
|
288
|
+
Functions:
|
|
289
|
+
api 128MB 30s https://your-kb.openkbs.com/api
|
|
290
|
+
auth 256MB 30s https://your-kb.openkbs.com/auth
|
|
291
|
+
posts 512MB 60s https://your-kb.openkbs.com/posts
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
## Delete Function
|
|
295
|
+
|
|
296
|
+
```bash
|
|
297
|
+
openkbs fn delete api
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
## CLI Reference
|
|
301
|
+
|
|
302
|
+
```bash
|
|
303
|
+
openkbs fn list # List functions
|
|
304
|
+
openkbs fn push <name> # Deploy function
|
|
305
|
+
openkbs fn push <name> --memory 512 # With memory
|
|
306
|
+
openkbs fn push <name> --timeout 60 # With timeout
|
|
307
|
+
openkbs fn logs <name> # View logs
|
|
308
|
+
openkbs fn logs <name> --follow # Stream logs
|
|
309
|
+
openkbs fn env <name> # View env vars
|
|
310
|
+
openkbs fn env <name> KEY=value # Set env var
|
|
311
|
+
openkbs fn invoke <name> '{}' # Test invoke
|
|
312
|
+
openkbs fn delete <name> # Delete function
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
## Tips
|
|
316
|
+
|
|
317
|
+
1. **Connection Reuse** - Initialize DB connections outside the handler for reuse.
|
|
318
|
+
|
|
319
|
+
2. **Cold Starts** - More memory = faster cold starts. 512MB is a good default.
|
|
320
|
+
|
|
321
|
+
3. **Timeout** - Set appropriate timeouts. Don't leave at 30s if your function takes 5s.
|
|
322
|
+
|
|
323
|
+
4. **Logs** - Use `console.log()` (Node.js), `print()` (Python), or `context.getLogger()` (Java).
|
|
324
|
+
|
|
325
|
+
## Next Steps
|
|
326
|
+
|
|
327
|
+
- [Tutorial 14: Real-time Pulse](./14-pulse.md)
|
|
328
|
+
- [Tutorial 15: Node.js Full Example](./15-nodejs-example.md)
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
# Tutorial 11: PostgreSQL Database
|
|
2
|
+
|
|
3
|
+
Add a PostgreSQL database to your project with one command. Elastic Postgres uses Neon - a serverless PostgreSQL that scales automatically.
|
|
4
|
+
|
|
5
|
+
## Enable Postgres
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
openkbs postgres enable
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
That's it. Your database is ready.
|
|
12
|
+
|
|
13
|
+
## Check Status
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
openkbs postgres status
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Output:
|
|
20
|
+
```
|
|
21
|
+
Postgres Status:
|
|
22
|
+
Enabled: true
|
|
23
|
+
Host: ep-xyz-123456.us-east-1.aws.neon.tech
|
|
24
|
+
Database: neondb
|
|
25
|
+
Region: us-east-1
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Get Connection String
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
openkbs postgres connection
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Output:
|
|
35
|
+
```
|
|
36
|
+
postgresql://user:password@ep-xyz-123456.us-east-1.aws.neon.tech/neondb?sslmode=require
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Use in Your Function
|
|
40
|
+
|
|
41
|
+
The `DATABASE_URL` environment variable is automatically injected into your Lambda functions.
|
|
42
|
+
|
|
43
|
+
### Node.js
|
|
44
|
+
|
|
45
|
+
```javascript
|
|
46
|
+
import pg from 'pg';
|
|
47
|
+
|
|
48
|
+
const db = new pg.Client({ connectionString: process.env.DATABASE_URL });
|
|
49
|
+
let connected = false;
|
|
50
|
+
|
|
51
|
+
export async function handler(event) {
|
|
52
|
+
// Connect once, reuse across invocations
|
|
53
|
+
if (!connected) {
|
|
54
|
+
await db.connect();
|
|
55
|
+
connected = true;
|
|
56
|
+
|
|
57
|
+
// Create tables on first run
|
|
58
|
+
await db.query(`
|
|
59
|
+
CREATE TABLE IF NOT EXISTS users (
|
|
60
|
+
id SERIAL PRIMARY KEY,
|
|
61
|
+
name TEXT NOT NULL,
|
|
62
|
+
email TEXT UNIQUE NOT NULL,
|
|
63
|
+
created_at TIMESTAMP DEFAULT NOW()
|
|
64
|
+
)
|
|
65
|
+
`);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const { action, ...data } = JSON.parse(event.body || '{}');
|
|
69
|
+
|
|
70
|
+
switch (action) {
|
|
71
|
+
case 'list':
|
|
72
|
+
const { rows } = await db.query('SELECT * FROM users ORDER BY created_at DESC');
|
|
73
|
+
return { statusCode: 200, body: JSON.stringify(rows) };
|
|
74
|
+
|
|
75
|
+
case 'create':
|
|
76
|
+
const result = await db.query(
|
|
77
|
+
'INSERT INTO users (name, email) VALUES ($1, $2) RETURNING *',
|
|
78
|
+
[data.name, data.email]
|
|
79
|
+
);
|
|
80
|
+
return { statusCode: 200, body: JSON.stringify(result.rows[0]) };
|
|
81
|
+
|
|
82
|
+
case 'delete':
|
|
83
|
+
await db.query('DELETE FROM users WHERE id = $1', [data.id]);
|
|
84
|
+
return { statusCode: 200, body: JSON.stringify({ deleted: true }) };
|
|
85
|
+
|
|
86
|
+
default:
|
|
87
|
+
return { statusCode: 400, body: JSON.stringify({ error: 'Unknown action' }) };
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
`package.json`:
|
|
93
|
+
```json
|
|
94
|
+
{
|
|
95
|
+
"type": "module",
|
|
96
|
+
"dependencies": {
|
|
97
|
+
"pg": "^8.11.3"
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### Python
|
|
103
|
+
|
|
104
|
+
```python
|
|
105
|
+
import json
|
|
106
|
+
import os
|
|
107
|
+
import psycopg2
|
|
108
|
+
|
|
109
|
+
conn = None
|
|
110
|
+
|
|
111
|
+
def handler(event, context):
|
|
112
|
+
global conn
|
|
113
|
+
if conn is None:
|
|
114
|
+
conn = psycopg2.connect(os.environ['DATABASE_URL'])
|
|
115
|
+
with conn.cursor() as cur:
|
|
116
|
+
cur.execute('''
|
|
117
|
+
CREATE TABLE IF NOT EXISTS items (
|
|
118
|
+
id SERIAL PRIMARY KEY,
|
|
119
|
+
name TEXT NOT NULL
|
|
120
|
+
)
|
|
121
|
+
''')
|
|
122
|
+
conn.commit()
|
|
123
|
+
|
|
124
|
+
body = json.loads(event.get('body', '{}'))
|
|
125
|
+
action = body.get('action')
|
|
126
|
+
|
|
127
|
+
with conn.cursor() as cur:
|
|
128
|
+
if action == 'list':
|
|
129
|
+
cur.execute('SELECT * FROM items')
|
|
130
|
+
rows = cur.fetchall()
|
|
131
|
+
return {'statusCode': 200, 'body': json.dumps(rows)}
|
|
132
|
+
|
|
133
|
+
elif action == 'create':
|
|
134
|
+
cur.execute('INSERT INTO items (name) VALUES (%s) RETURNING id', (body['name'],))
|
|
135
|
+
conn.commit()
|
|
136
|
+
return {'statusCode': 200, 'body': json.dumps({'id': cur.fetchone()[0]})}
|
|
137
|
+
|
|
138
|
+
return {'statusCode': 400, 'body': json.dumps({'error': 'Unknown action'})}
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
### Java
|
|
142
|
+
|
|
143
|
+
```java
|
|
144
|
+
package com.example;
|
|
145
|
+
|
|
146
|
+
import com.amazonaws.services.lambda.runtime.Context;
|
|
147
|
+
import com.amazonaws.services.lambda.runtime.RequestHandler;
|
|
148
|
+
import com.google.gson.Gson;
|
|
149
|
+
import java.sql.*;
|
|
150
|
+
import java.util.*;
|
|
151
|
+
|
|
152
|
+
public class Handler implements RequestHandler<Map<String, Object>, Map<String, Object>> {
|
|
153
|
+
private static Connection conn;
|
|
154
|
+
private static final Gson gson = new Gson();
|
|
155
|
+
|
|
156
|
+
@Override
|
|
157
|
+
public Map<String, Object> handleRequest(Map<String, Object> event, Context context) {
|
|
158
|
+
try {
|
|
159
|
+
if (conn == null || conn.isClosed()) {
|
|
160
|
+
conn = DriverManager.getConnection(System.getenv("DATABASE_URL"));
|
|
161
|
+
try (Statement stmt = conn.createStatement()) {
|
|
162
|
+
stmt.execute("CREATE TABLE IF NOT EXISTS items (id SERIAL PRIMARY KEY, name TEXT)");
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
String body = (String) event.get("body");
|
|
167
|
+
Map<String, Object> request = gson.fromJson(body, Map.class);
|
|
168
|
+
String action = (String) request.get("action");
|
|
169
|
+
|
|
170
|
+
if ("list".equals(action)) {
|
|
171
|
+
List<Map<String, Object>> items = new ArrayList<>();
|
|
172
|
+
try (ResultSet rs = conn.createStatement().executeQuery("SELECT * FROM items")) {
|
|
173
|
+
while (rs.next()) {
|
|
174
|
+
items.add(Map.of("id", rs.getInt("id"), "name", rs.getString("name")));
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
return Map.of("statusCode", 200, "body", gson.toJson(items));
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return Map.of("statusCode", 400, "body", "{\"error\":\"Unknown action\"}");
|
|
181
|
+
} catch (Exception e) {
|
|
182
|
+
return Map.of("statusCode", 500, "body", "{\"error\":\"" + e.getMessage() + "\"}");
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
## Common Patterns
|
|
189
|
+
|
|
190
|
+
### Parameterized Queries (Prevent SQL Injection)
|
|
191
|
+
|
|
192
|
+
```javascript
|
|
193
|
+
// GOOD - parameterized
|
|
194
|
+
await db.query('SELECT * FROM users WHERE id = $1', [userId]);
|
|
195
|
+
|
|
196
|
+
// BAD - string concatenation
|
|
197
|
+
await db.query(`SELECT * FROM users WHERE id = ${userId}`); // NEVER DO THIS
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
### Pagination
|
|
201
|
+
|
|
202
|
+
```javascript
|
|
203
|
+
const page = data.page || 1;
|
|
204
|
+
const limit = data.limit || 20;
|
|
205
|
+
const offset = (page - 1) * limit;
|
|
206
|
+
|
|
207
|
+
const { rows } = await db.query(
|
|
208
|
+
'SELECT * FROM items ORDER BY created_at DESC LIMIT $1 OFFSET $2',
|
|
209
|
+
[limit, offset]
|
|
210
|
+
);
|
|
211
|
+
|
|
212
|
+
const { rows: [{ count }] } = await db.query('SELECT COUNT(*) FROM items');
|
|
213
|
+
|
|
214
|
+
return { items: rows, total: parseInt(count), page, limit };
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
### Search
|
|
218
|
+
|
|
219
|
+
```javascript
|
|
220
|
+
const { rows } = await db.query(
|
|
221
|
+
'SELECT * FROM items WHERE name ILIKE $1',
|
|
222
|
+
[`%${data.query}%`]
|
|
223
|
+
);
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
### JSON Columns
|
|
227
|
+
|
|
228
|
+
```javascript
|
|
229
|
+
// Create table with JSONB column
|
|
230
|
+
await db.query(`
|
|
231
|
+
CREATE TABLE IF NOT EXISTS products (
|
|
232
|
+
id SERIAL PRIMARY KEY,
|
|
233
|
+
name TEXT,
|
|
234
|
+
metadata JSONB DEFAULT '{}'
|
|
235
|
+
)
|
|
236
|
+
`);
|
|
237
|
+
|
|
238
|
+
// Query JSON
|
|
239
|
+
const { rows } = await db.query(
|
|
240
|
+
"SELECT * FROM products WHERE metadata->>'category' = $1",
|
|
241
|
+
['electronics']
|
|
242
|
+
);
|
|
243
|
+
|
|
244
|
+
// Update JSON field
|
|
245
|
+
await db.query(
|
|
246
|
+
"UPDATE products SET metadata = jsonb_set(metadata, '{stock}', $1) WHERE id = $2",
|
|
247
|
+
[JSON.stringify(100), productId]
|
|
248
|
+
);
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
### Transactions
|
|
252
|
+
|
|
253
|
+
```javascript
|
|
254
|
+
try {
|
|
255
|
+
await db.query('BEGIN');
|
|
256
|
+
await db.query('UPDATE accounts SET balance = balance - $1 WHERE id = $2', [amount, fromId]);
|
|
257
|
+
await db.query('UPDATE accounts SET balance = balance + $1 WHERE id = $2', [amount, toId]);
|
|
258
|
+
await db.query('COMMIT');
|
|
259
|
+
} catch (e) {
|
|
260
|
+
await db.query('ROLLBACK');
|
|
261
|
+
throw e;
|
|
262
|
+
}
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
## CLI Reference
|
|
266
|
+
|
|
267
|
+
```bash
|
|
268
|
+
openkbs postgres enable # Enable database
|
|
269
|
+
openkbs postgres status # Check status
|
|
270
|
+
openkbs postgres connection # Get connection string
|
|
271
|
+
openkbs postgres disable # Disable (WARNING: deletes data)
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
## Tips
|
|
275
|
+
|
|
276
|
+
1. **Connection Reuse** - Keep `db.connect()` outside the handler. Lambda reuses the connection across invocations.
|
|
277
|
+
|
|
278
|
+
2. **Cold Starts** - Neon is serverless. First connection after idle may take 1-2 seconds.
|
|
279
|
+
|
|
280
|
+
3. **Create Tables on Init** - Use `CREATE TABLE IF NOT EXISTS` in your connection logic.
|
|
281
|
+
|
|
282
|
+
4. **Always Use Parameters** - Never concatenate user input into SQL strings.
|
|
283
|
+
|
|
284
|
+
## Next Steps
|
|
285
|
+
|
|
286
|
+
- [Tutorial 12: S3 Storage](./12-storage.md)
|
|
287
|
+
- [Tutorial 13: Serverless Functions](./13-functions.md)
|