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.
@@ -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)