create-surf-app 0.1.14 → 0.1.15

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.
@@ -1,20 +1,8 @@
1
- /**
2
- * Drizzle ORM schema definitions.
3
- *
4
- * Define your tables here using Drizzle's pgTable builder.
5
- * This file is the single source of truth for your database schema.
6
- *
7
- * Example:
8
- * const { pgTable, serial, text, boolean, timestamp } = require('drizzle-orm/pg-core');
9
- *
10
- * const todos = pgTable('todos', {
11
- * id: serial('id').primaryKey(),
12
- * title: text('title').notNull(),
13
- * completed: boolean('completed').default(false),
14
- * createdAt: timestamp('created_at', { withTimezone: true }).defaultNow(),
15
- * });
16
- *
17
- * module.exports = { todos };
18
- */
19
-
20
- // Add your table definitions here.
1
+ // Define your Drizzle ORM tables here.
2
+ // Example:
3
+ // const { pgTable, serial, text, timestamp } = require('drizzle-orm/pg-core')
4
+ // exports.users = pgTable('users', {
5
+ // id: serial('id').primaryKey(),
6
+ // name: text('name').notNull(),
7
+ // created_at: timestamp('created_at').defaultNow(),
8
+ // })
@@ -2,7 +2,7 @@ import js from '@eslint/js';
2
2
  import globals from 'globals';
3
3
 
4
4
  export default [
5
- { ignores: ['node_modules', 'lib/api*.js'] },
5
+ { ignores: ['node_modules'] },
6
6
  {
7
7
  files: ['**/*.js'],
8
8
  ...js.configs.recommended,
@@ -1,26 +1,12 @@
1
1
  {
2
2
  "name": "backend",
3
3
  "private": true,
4
- "version": "0.0.0",
5
4
  "scripts": {
6
- "check": "node --check server.js && find routes lib db -type f -name '*.js' -print0 | xargs -0 -n1 node --check",
7
- "lint": "eslint .",
8
5
  "start": "node server.js",
9
- "dev": "node --watch --watch-path=./routes --watch-path=./lib --watch-path=./server.js server.js",
10
- "verify": "npm run lint && npm run check"
6
+ "dev": "node --watch server.js"
11
7
  },
12
8
  "dependencies": {
13
9
  "@surf-ai/sdk": "0.1.4-beta",
14
- "express": "4.22.1",
15
- "cors": "2.8.6",
16
- "http-proxy-middleware": "3.0.5",
17
- "drizzle-orm": "0.44.7",
18
- "drizzle-kit": "0.30.6",
19
- "croner": "9.1.0"
20
- },
21
- "devDependencies": {
22
- "@eslint/js": "9.39.4",
23
- "eslint": "9.39.4",
24
- "globals": "16.5.0"
10
+ "express": "4.22.1"
25
11
  }
26
12
  }
@@ -1,444 +1,2 @@
1
- const express = require('express');
2
- const cors = require('cors');
3
- const { Cron } = require('croner');
4
- const fs = require('fs');
5
- const path = require('path');
6
- const { setupProxyRoutes } = require('./routes/proxy');
7
-
8
- if (!process.env.PORT) {
9
- const envPath = path.join(__dirname, '.env');
10
- if (fs.existsSync(envPath)) {
11
- for (const line of fs.readFileSync(envPath, 'utf8').split(/\r?\n/)) {
12
- const trimmed = line.trim();
13
- if (!trimmed || trimmed.startsWith('#')) continue;
14
- const idx = trimmed.indexOf('=');
15
- if (idx === -1) continue;
16
- const key = trimmed.slice(0, idx).trim();
17
- const value = trimmed.slice(idx + 1).trim();
18
- if (key && !(key in process.env)) {
19
- process.env[key] = value;
20
- }
21
- }
22
- }
23
- }
24
-
25
- const app = express();
26
- const PORT = Number.parseInt(process.env.PORT || '', 10);
27
- if (!Number.isInteger(PORT)) {
28
- throw new Error('PORT env var is required');
29
- }
30
-
31
- // CORS: allow any origin (external frontends can connect)
32
- app.use(cors());
33
-
34
- // Forward /proxy/* → OutboundProxy (API key injection)
35
- // NOTE: Must be registered BEFORE express.json() so that the raw request body
36
- // stream is preserved for http-proxy-middleware to forward POST requests.
37
- setupProxyRoutes(app);
38
-
39
- // Parse JSON bodies (after proxy routes to avoid consuming body stream)
40
- app.use(express.json());
41
-
42
- // Health check
43
- app.get('/api/health', (_req, res) => {
44
- res.json({ status: 'ok' });
45
- });
46
-
47
- // ── Auto-register API routes ─────────────────────────────────────────────
48
- // Every .js file in routes/ (except proxy.js) is auto-mounted at /api/{name}.
49
- // e.g. routes/todos.js → /api/todos, routes/auth.js → /api/auth
50
- const routesDir = path.join(__dirname, 'routes');
51
- for (const file of fs.readdirSync(routesDir)) {
52
- if (file === 'proxy.js' || !file.endsWith('.js')) continue;
53
- const name = file.replace('.js', '');
54
- try {
55
- app.use(`/api/${name}`, require(`./routes/${file}`));
56
- console.log(`Route registered: /api/${name}`);
57
- } catch (err) {
58
- console.error(`Failed to load route ${file}: ${err.message}`);
59
- }
60
- }
61
-
62
- // ── Auto-create DB tables from schema.js ─────────────────────────────────
63
- // The agent writes db/schema.js AFTER the server starts, so we need two
64
- // trigger paths: (1) POST /api/__sync-schema for explicit agent calls,
65
- // (2) fs.watchFile as a fallback in case the agent doesn't call the endpoint.
66
- //
67
- // Both paths funnel into doSyncSchema() which has:
68
- // - Concurrency lock (syncing flag) to prevent overlapping DDL
69
- // - SyntaxError guard for half-written files
70
- // - Retry with backoff for Neon cold-start errors
71
- let schemaReady = false;
72
- let syncing = false;
73
-
74
- async function doSyncSchema() {
75
- if (syncing) return;
76
- syncing = true;
77
- try {
78
- // Clear require cache so we pick up the latest schema.js
79
- try { delete require.cache[require.resolve('./db/schema')]; } catch (_e) { /* not cached — ignore */ }
80
-
81
- // Try to load schema — SyntaxError means the file is half-written
82
- let schema;
83
- try {
84
- schema = require('./db/schema');
85
- } catch (err) {
86
- if (err instanceof SyntaxError) {
87
- console.log('DB: schema.js has syntax error, waiting for next change...');
88
- return;
89
- }
90
- if (err.message.includes('Cannot find module') || err.message.includes('is not a function')) {
91
- return; // No schema or no drizzle-orm — skip silently
92
- }
93
- throw err;
94
- }
95
-
96
- const { getTableConfig } = require('drizzle-orm/pg-core');
97
- const tables = Object.values(schema).filter(t =>
98
- t && typeof t === 'object' && Symbol.for('drizzle:Name') in t
99
- );
100
- if (tables.length === 0) return;
101
-
102
- const { dbTables, dbProvision, dbQuery, dbTableSchema } = require('./lib/db');
103
- await dbProvision('auto-sync schema tables');
104
-
105
- const existing = (await dbTables()).map(t => t.name);
106
- const missing = tables.filter(t => !existing.includes(getTableConfig(t).name));
107
-
108
- if (missing.length > 0) {
109
- // Use drizzle-kit to generate correct DDL (handles UNIQUE, FK, defaults, etc.)
110
- const { generateDrizzleJson, generateMigration } = require('drizzle-kit/api');
111
- const missingSchema = {};
112
- for (const t of missing) missingSchema[getTableConfig(t).name] = t;
113
- const sqls = await generateMigration(generateDrizzleJson({}), generateDrizzleJson(missingSchema));
114
-
115
- for (const sql of sqls) {
116
- for (let attempt = 0; attempt < 2; attempt++) {
117
- try {
118
- await dbQuery(sql, []);
119
- console.log(`DB: Executed: ${sql.slice(0, 80)}...`);
120
- break;
121
- } catch (err) {
122
- if (attempt === 0) {
123
- console.warn(`DB: Retrying after: ${err.message}`);
124
- await new Promise(r => setTimeout(r, 1500));
125
- } else {
126
- console.error(`DB: Failed: ${sql.slice(0, 80)}... — ${err.message}`);
127
- }
128
- }
129
- }
130
- }
131
- }
132
-
133
- // Check existing tables for missing columns (runs even when new tables were also created)
134
- const existingTables = tables.filter(t => existing.includes(getTableConfig(t).name));
135
- for (const t of existingTables) {
136
- const cfg = getTableConfig(t);
137
- try {
138
- const live = await dbTableSchema(cfg.name);
139
- const liveCols = new Set(live.columns.map(c => c.name));
140
- for (const col of cfg.columns) {
141
- if (!liveCols.has(col.name)) {
142
- const colType = col.getSQLType();
143
- const ddl = 'ALTER TABLE "' + cfg.name + '" ADD COLUMN IF NOT EXISTS "' + col.name + '" ' + colType;
144
- try {
145
- await dbQuery(ddl, []);
146
- console.log('DB: Added column ' + col.name + ' to ' + cfg.name);
147
- } catch (err) {
148
- console.warn('DB: Failed to add column ' + col.name + ' to ' + cfg.name + ': ' + err.message);
149
- }
150
- }
151
- }
152
- } catch (err) {
153
- console.warn('DB: Column check failed for ' + cfg.name + ': ' + err.message);
154
- }
155
- }
156
- } finally {
157
- syncing = false;
158
- }
159
- }
160
-
161
- // Retry wrapper for Neon cold-start transient errors.
162
- async function syncSchemaWithRetry(retries = 3, delay = 2000) {
163
- for (let i = 0; i < retries; i++) {
164
- try {
165
- await doSyncSchema();
166
- return;
167
- } catch (err) {
168
- console.error(`DB schema sync attempt ${i + 1}/${retries} failed: ${err.message}`);
169
- if (i < retries - 1) await new Promise(r => setTimeout(r, delay * (i + 1)));
170
- }
171
- }
172
- console.error('DB schema sync failed after all retries');
173
- }
174
-
175
- // Explicit sync endpoint — agent calls this after writing schema.js.
176
- app.post('/api/__sync-schema', async (_req, res) => {
177
- try {
178
- await syncSchemaWithRetry(2, 1500);
179
- res.json({ ok: true });
180
- } catch (err) {
181
- res.status(500).json({ ok: false, error: err.message });
182
- }
183
- });
184
-
185
- // ── Cron job system ──────────────────────────────────────────────────────
186
- // Reads backend/cron.json, schedules tasks with croner, exposes CRUD API.
187
- const _cronInstances = new Map(); // id -> Cron instance
188
- const _cronState = new Map(); // id -> { lastRunAt, lastStatus, lastError }
189
-
190
- function _loadCronJobs() {
191
- // Stop all existing cron jobs
192
- for (const [, job] of _cronInstances) {
193
- try { job.stop(); } catch (_e) { /* already stopped — ignore */ }
194
- }
195
- _cronInstances.clear();
196
-
197
- const cronPath = path.join(__dirname, 'cron.json');
198
- if (!fs.existsSync(cronPath)) return;
199
-
200
- let tasks;
201
- try {
202
- tasks = JSON.parse(fs.readFileSync(cronPath, 'utf8'));
203
- } catch (err) {
204
- console.error('Cron: failed to parse cron.json:', err.message);
205
- return;
206
- }
207
-
208
- if (!Array.isArray(tasks)) {
209
- console.error('Cron: cron.json must be a JSON array');
210
- return;
211
- }
212
-
213
- for (const task of tasks) {
214
- if (!task.enabled) continue;
215
- if (!task.id || !task.schedule || !task.handler) {
216
- console.warn('Cron: skipping task with missing fields:', JSON.stringify(task));
217
- continue;
218
- }
219
-
220
- let handlerMod;
221
- try {
222
- const handlerPath = path.resolve(__dirname, task.handler);
223
- delete require.cache[handlerPath];
224
- handlerMod = require(handlerPath);
225
- } catch (err) {
226
- console.error(`Cron: failed to load handler ${task.handler}: ${err.message}`);
227
- continue;
228
- }
229
-
230
- if (typeof handlerMod.handler !== 'function') {
231
- console.warn(`Cron: handler ${task.handler} does not export a handler function`);
232
- continue;
233
- }
234
-
235
- const timeoutMs = (task.timeout || 300) * 1000;
236
-
237
- // Initialize runtime state if not present
238
- if (!_cronState.has(task.id)) {
239
- _cronState.set(task.id, { lastRunAt: null, lastStatus: null, lastError: null });
240
- }
241
-
242
- const runHandler = async () => {
243
- const state = _cronState.get(task.id);
244
- state.lastRunAt = new Date().toISOString();
245
- try {
246
- await Promise.race([
247
- handlerMod.handler(),
248
- new Promise((_, reject) => setTimeout(() => reject(new Error('timeout')), timeoutMs)),
249
- ]);
250
- state.lastStatus = 'success';
251
- state.lastError = null;
252
- console.log(`Cron: task ${task.id} completed successfully`);
253
- } catch (err) {
254
- state.lastStatus = 'error';
255
- state.lastError = err.message;
256
- console.error(`Cron: task ${task.id} failed: ${err.message}`);
257
- }
258
- };
259
-
260
- try {
261
- const job = new Cron(task.schedule, runHandler);
262
- _cronInstances.set(task.id, job);
263
- console.log(`Cron: scheduled ${task.id} (${task.name || task.id}) with schedule "${task.schedule}"`);
264
- } catch (err) {
265
- console.error(`Cron: invalid schedule for ${task.id}: ${err.message}`);
266
- }
267
- }
268
- }
269
-
270
- // ── Cron CRUD API routes (before schema readiness guard) ─────────────────
271
- // Auth middleware for /api/cron routes
272
- app.use('/api/cron', (req, res, next) => {
273
- const appToken = process.env.APP_TOKEN;
274
- if (!appToken) return next(); // dev mode: skip auth
275
- const auth = req.headers.authorization;
276
- if (!auth || auth !== `Bearer ${appToken}`) {
277
- return res.status(401).json({ error: 'Unauthorized' });
278
- }
279
- next();
280
- });
281
-
282
- app.get('/api/cron', (_req, res) => {
283
- const cronPath = path.join(__dirname, 'cron.json');
284
- let tasks = [];
285
- try {
286
- if (fs.existsSync(cronPath)) {
287
- tasks = JSON.parse(fs.readFileSync(cronPath, 'utf8'));
288
- }
289
- } catch { tasks = []; }
290
-
291
- const result = tasks.map(t => {
292
- const state = _cronState.get(t.id) || { lastRunAt: null, lastStatus: null, lastError: null };
293
- const job = _cronInstances.get(t.id);
294
- return {
295
- ...t,
296
- lastRunAt: state.lastRunAt,
297
- lastStatus: state.lastStatus,
298
- lastError: state.lastError,
299
- nextRun: job ? job.nextRun()?.toISOString() || null : null,
300
- };
301
- });
302
- res.json(result);
303
- });
304
-
305
- app.post('/api/cron', (req, res) => {
306
- const { id, name, schedule, handler, enabled = true, timeout = 300 } = req.body || {};
307
- if (!id || !name || !schedule || !handler) {
308
- return res.status(400).json({ error: 'Missing required fields: id, name, schedule, handler' });
309
- }
310
-
311
- // Validate cron expression and minimum interval (>= 1 minute)
312
- try {
313
- const testJob = new Cron(schedule);
314
- const next1 = testJob.nextRun();
315
- const next2 = testJob.nextRun(next1);
316
- testJob.stop();
317
- if (next1 && next2 && (next2.getTime() - next1.getTime()) < 60000) {
318
- return res.status(400).json({ error: 'Minimum interval between runs must be at least 1 minute' });
319
- }
320
- } catch (err) {
321
- return res.status(400).json({ error: `Invalid cron expression: ${err.message}` });
322
- }
323
-
324
- const cronPath = path.join(__dirname, 'cron.json');
325
- let tasks = [];
326
- try {
327
- if (fs.existsSync(cronPath)) tasks = JSON.parse(fs.readFileSync(cronPath, 'utf8'));
328
- } catch { tasks = []; }
329
-
330
- if (tasks.some(t => t.id === id)) {
331
- return res.status(409).json({ error: `Task with id "${id}" already exists` });
332
- }
333
-
334
- const newTask = { id, name, schedule, handler, enabled, timeout };
335
- tasks.push(newTask);
336
- fs.writeFileSync(cronPath, JSON.stringify(tasks, null, 2));
337
- _loadCronJobs();
338
- res.status(201).json(newTask);
339
- });
340
-
341
- app.patch('/api/cron/:id', (req, res) => {
342
- const cronPath = path.join(__dirname, 'cron.json');
343
- let tasks = [];
344
- try {
345
- if (fs.existsSync(cronPath)) tasks = JSON.parse(fs.readFileSync(cronPath, 'utf8'));
346
- } catch { tasks = []; }
347
-
348
- const idx = tasks.findIndex(t => t.id === req.params.id);
349
- if (idx === -1) return res.status(404).json({ error: 'Task not found' });
350
-
351
- const updates = req.body || {};
352
-
353
- // If schedule is being updated, validate it
354
- if (updates.schedule) {
355
- try {
356
- const testJob = new Cron(updates.schedule);
357
- const next1 = testJob.nextRun();
358
- const next2 = testJob.nextRun(next1);
359
- testJob.stop();
360
- if (next1 && next2 && (next2.getTime() - next1.getTime()) < 60000) {
361
- return res.status(400).json({ error: 'Minimum interval between runs must be at least 1 minute' });
362
- }
363
- } catch (err) {
364
- return res.status(400).json({ error: `Invalid cron expression: ${err.message}` });
365
- }
366
- }
367
-
368
- tasks[idx] = { ...tasks[idx], ...updates, id: req.params.id };
369
- fs.writeFileSync(cronPath, JSON.stringify(tasks, null, 2));
370
- _loadCronJobs();
371
- res.json(tasks[idx]);
372
- });
373
-
374
- app.delete('/api/cron/:id', (req, res) => {
375
- const cronPath = path.join(__dirname, 'cron.json');
376
- let tasks = [];
377
- try {
378
- if (fs.existsSync(cronPath)) tasks = JSON.parse(fs.readFileSync(cronPath, 'utf8'));
379
- } catch { tasks = []; }
380
-
381
- const idx = tasks.findIndex(t => t.id === req.params.id);
382
- if (idx === -1) return res.status(404).json({ error: 'Task not found' });
383
-
384
- tasks.splice(idx, 1);
385
- fs.writeFileSync(cronPath, JSON.stringify(tasks, null, 2));
386
- _cronState.delete(req.params.id);
387
- _loadCronJobs();
388
- res.json({ ok: true });
389
- });
390
-
391
- app.post('/api/cron/:id/run', async (req, res) => {
392
- const job = _cronInstances.get(req.params.id);
393
- if (!job) return res.status(404).json({ error: 'Task not found or not enabled' });
394
- try {
395
- job.trigger();
396
- res.json({ ok: true, message: `Task ${req.params.id} triggered` });
397
- } catch (err) {
398
- res.status(500).json({ error: err.message });
399
- }
400
- });
401
-
402
- // Block /api/* (except health + sync + cron) until initial schema sync completes.
403
- app.use('/api', (req, res, next) => {
404
- if (schemaReady || req.path === '/health' || req.path === '/__sync-schema' || req.path.startsWith('/cron')) return next();
405
- res.status(503).json({ error: 'Database schema initializing...' });
406
- });
407
-
408
- function startServer(retries = 10, delay = 1000) {
409
- const server = app.listen(PORT, '0.0.0.0', async () => {
410
- console.log(`Backend listening on port ${PORT}`);
411
- await syncSchemaWithRetry();
412
- schemaReady = true;
413
- console.log('Schema sync complete, API ready');
414
-
415
- // Load cron jobs after DB is ready
416
- _loadCronJobs();
417
-
418
- // Fallback: watch schema.js for changes in case agent doesn't call __sync-schema.
419
- const schemaPath = path.join(__dirname, 'db', 'schema.js');
420
- let syncDebounce = null;
421
- fs.watchFile(schemaPath, { interval: 2000 }, () => {
422
- if (syncDebounce) clearTimeout(syncDebounce);
423
- syncDebounce = setTimeout(async () => {
424
- console.log('DB: schema.js changed, re-syncing tables...');
425
- try {
426
- await syncSchemaWithRetry(2, 1500);
427
- console.log('DB: schema re-sync complete');
428
- } catch (err) {
429
- console.error(`DB: schema re-sync failed: ${err.message}`);
430
- }
431
- }, 1000);
432
- });
433
- });
434
- server.on('error', (err) => {
435
- if (err.code === 'EADDRINUSE' && retries > 0) {
436
- console.log(`Port ${PORT} in use, retrying in ${delay}ms... (${retries} left)`);
437
- setTimeout(() => startServer(retries - 1, delay), delay);
438
- } else {
439
- console.error(`Failed to start server: ${err.message}`);
440
- process.exit(1);
441
- }
442
- });
443
- }
444
- startServer();
1
+ const { createServer } = require('@surf-ai/sdk/server')
2
+ createServer().start()
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-surf-app",
3
- "version": "0.1.14",
3
+ "version": "0.1.15",
4
4
  "description": "Scaffold a Surf app — Vite + React + Express + @surf-ai/sdk",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,23 +0,0 @@
1
- /**
2
- * Drizzle ORM client (pg-proxy driver).
3
- *
4
- * Routes all SQL through hermod's DB proxy — works identically in
5
- * sandbox (OutboundProxy) and deployed mode (hermod gateway).
6
- *
7
- * Usage:
8
- * const { db } = require('./db');
9
- * const { todos } = require('./db/schema');
10
- * const rows = await db.select().from(todos);
11
- */
12
-
13
- const { drizzle } = require('drizzle-orm/pg-proxy');
14
- const { dbQuery } = require('../lib/db');
15
-
16
- const db = drizzle(async (sql, params, method) => {
17
- const result = await dbQuery(sql, params, { arrayMode: true });
18
- // pg-proxy expects rows as positional arrays — arrayMode makes hermod
19
- // return [[val1, val2, ...], ...] instead of [{col: val}, ...].
20
- return { rows: result.rows || [] };
21
- });
22
-
23
- module.exports = { db };
@@ -1,67 +0,0 @@
1
- /**
2
- * Server-side database helpers.
3
- *
4
- * All SQL execution happens here in the backend — NEVER in frontend code.
5
- * Calls /proxy/db/* via loopback through the Express proxy middleware,
6
- * which handles both sandbox mode (OutboundProxy) and deployed mode (hermod).
7
- */
8
-
9
- const PORT = Number.parseInt(process.env.PORT || '', 10);
10
- if (!Number.isInteger(PORT)) {
11
- throw new Error('PORT env var is required');
12
- }
13
- const BASE = `http://127.0.0.1:${PORT}/proxy/db`;
14
-
15
- async function request(path, options = {}) {
16
- const res = await fetch(`${BASE}${path}`, options);
17
- if (!res.ok) {
18
- const body = await res.json().catch(() => ({ message: res.statusText }));
19
- const err = new Error(body.detail || body.error || body.message || `DB request failed: ${res.status}`);
20
- err.status = res.status;
21
- throw err;
22
- }
23
- return res.json();
24
- }
25
-
26
- /** Provision a database for the current user (idempotent). */
27
- async function dbProvision(reason) {
28
- return request('/provision', {
29
- method: 'POST',
30
- headers: { 'Content-Type': 'application/json' },
31
- body: JSON.stringify({ reason: reason || '' }),
32
- });
33
- }
34
-
35
- /**
36
- * Execute a parameterized SQL query.
37
- * ALWAYS use $1, $2, ... placeholders — NEVER concatenate user input.
38
- * @param {string} sql
39
- * @param {any[]} params
40
- * @param {{ arrayMode?: boolean }} [options] - Pass { arrayMode: true } for Drizzle pg-proxy
41
- */
42
- async function dbQuery(sql, params = [], options = {}) {
43
- return request('/query', {
44
- method: 'POST',
45
- headers: { 'Content-Type': 'application/json' },
46
- body: JSON.stringify({ sql, params, ...options }),
47
- });
48
- }
49
-
50
- /** List all tables with approximate row counts. */
51
- async function dbTables() {
52
- const data = await request('/tables');
53
- return data.tables;
54
- }
55
-
56
- /** Get column definitions for a table. */
57
- async function dbTableSchema(name) {
58
- const data = await request(`/tables/${encodeURIComponent(name)}/schema`);
59
- return data.columns;
60
- }
61
-
62
- /** Check database connection status and usage. */
63
- async function dbStatus() {
64
- return request('/status');
65
- }
66
-
67
- module.exports = { dbProvision, dbQuery, dbTables, dbTableSchema, dbStatus };
@@ -1,66 +0,0 @@
1
- /**
2
- * Catch-all proxy to data APIs.
3
- *
4
- * Sandbox mode: forwards /proxy/* to OutboundProxy at 127.0.0.1:9999
5
- * Deployed mode: transparent pass-through /proxy/{path} -> hermod /gateway/v1/{path}
6
- *
7
- * IMPORTANT: All proxy middlewares use selfHandleResponse + responseInterceptor
8
- * to buffer the full upstream response before sending. This forces Express to
9
- * respond with Content-Length instead of Transfer-Encoding: chunked, which is
10
- * required for compatibility with the KEDA HTTP interceptor (it drops chunked
11
- * response bodies when the upstream sends Connection: close).
12
- */
13
-
14
- const { createProxyMiddleware, responseInterceptor } = require('http-proxy-middleware');
15
-
16
- const GATEWAY_URL = process.env.GATEWAY_URL;
17
- const APP_TOKEN = process.env.APP_TOKEN;
18
- const IS_DEPLOYED = Boolean(GATEWAY_URL && APP_TOKEN);
19
-
20
- // Shared response interceptor: buffer full response so Express sends Content-Length
21
- // instead of Transfer-Encoding: chunked (fixes KEDA HTTP interceptor body-drop bug).
22
- const bufferResponse = responseInterceptor(async (responseBuffer, _proxyRes, _req, _res) => {
23
- return responseBuffer;
24
- });
25
-
26
- function setupProxyRoutes(app) {
27
- if (IS_DEPLOYED) {
28
- // Transparent pass-through: /proxy/{path} -> hermod /gateway/v1/{path}
29
- // pathRewrite strips /proxy prefix and prepends /gateway/v1
30
- app.use('/proxy', createProxyMiddleware({
31
- target: GATEWAY_URL,
32
- changeOrigin: true,
33
- selfHandleResponse: true,
34
- pathRewrite: (p) => '/gateway/v1' + p,
35
- headers: { Authorization: `Bearer ${APP_TOKEN}`, 'Accept-Encoding': 'identity' },
36
- on: { proxyRes: bufferResponse },
37
- }));
38
- } else {
39
- // Sandbox: forward to OutboundProxy (handles all routing internally)
40
- app.use(
41
- createProxyMiddleware({
42
- target: process.env.DATA_PROXY_BASE ? process.env.DATA_PROXY_BASE.replace(/\/proxy$/, '') : 'http://127.0.0.1:9999',
43
- changeOrigin: true,
44
- selfHandleResponse: true,
45
- pathFilter: '/proxy',
46
- on: {
47
- proxyReq: (proxyReq, req) => {
48
- console.log(`[proxy] >> ${req.method} ${req.originalUrl}`);
49
- },
50
- proxyRes: responseInterceptor(async (responseBuffer, proxyRes, req, _res) => {
51
- const status = proxyRes.statusCode;
52
- const tag = status >= 400 ? 'ERR' : 'OK';
53
- console.log(`[proxy] << ${status} ${tag} ${req.method} ${req.originalUrl} bytes=${responseBuffer.length}`);
54
- return responseBuffer;
55
- }),
56
- error: (err, req, res) => {
57
- console.error(`[proxy] !! ${req.method} ${req.originalUrl} error: ${err.message}`);
58
- if (!res.headersSent) res.status(502).json({ error: err.message });
59
- },
60
- },
61
- })
62
- );
63
- }
64
- }
65
-
66
- module.exports = { setupProxyRoutes };