Skip to content

Commit a2d52ce

Browse files
feat: Add database test endpoint for Railway testing
1 parent 3ec5ff2 commit a2d52ce

File tree

4 files changed

+323
-5
lines changed

4 files changed

+323
-5
lines changed

Dockerfile

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Railway Optimized Dockerfile
1+
# Railway Optimized Dockerfile - Minimal Dependencies
22
FROM node:20-slim
33

44
# Install dependencies for build
@@ -11,11 +11,18 @@ RUN apt-get update && apt-get install -y \
1111
# Set working directory
1212
WORKDIR /app
1313

14-
# Copy package files
14+
# Copy package files and use Railway-specific if available
1515
COPY package*.json ./
16+
COPY package.railway.json ./package.railway.json 2>/dev/null || true
1617

17-
# Install dependencies with clean cache
18-
RUN npm ci --legacy-peer-deps && \
18+
# Use Railway package.json if it exists (minimal dependencies)
19+
RUN if [ -f "package.railway.json" ]; then \
20+
echo "Using Railway-specific package.json"; \
21+
mv package.railway.json package.json; \
22+
fi
23+
24+
# Install only production dependencies
25+
RUN npm ci --omit=dev --no-audit --no-fund && \
1926
npm cache clean --force
2027

2128
# Copy source code
@@ -24,6 +31,9 @@ COPY . .
2431
# Build the application
2532
RUN npm run build
2633

34+
# Clean up build dependencies
35+
RUN rm -rf src/ scripts/ test/ tests/ __tests__ *.test.* *.spec.*
36+
2737
# Expose port
2838
EXPOSE 3000
2939

package.railway.json

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
{
2+
"name": "@stackmemoryai/stackmemory-railway",
3+
"version": "0.3.17",
4+
"description": "Railway deployment package",
5+
"type": "module",
6+
"main": "dist/servers/railway/minimal.js",
7+
"scripts": {
8+
"start": "node dist/servers/railway/minimal.js",
9+
"build": "node esbuild.config.js"
10+
},
11+
"dependencies": {
12+
"express": "^4.21.2",
13+
"cors": "^2.8.5",
14+
"redis": "^4.7.0",
15+
"pg": "^8.13.1",
16+
"better-sqlite3": "^11.7.0",
17+
"dotenv": "^17.2.3",
18+
"@modelcontextprotocol/sdk": "^1.0.6"
19+
},
20+
"devDependencies": {
21+
"esbuild": "^0.24.2",
22+
"@types/node": "^22.5.0",
23+
"typescript": "^5.7.3"
24+
},
25+
"engines": {
26+
"node": ">=20.0.0"
27+
}
28+
}

scripts/test-railway-db.js

Lines changed: 222 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,222 @@
1+
#!/usr/bin/env node
2+
import 'dotenv/config';
3+
import pg from 'pg';
4+
import { createClient } from 'redis';
5+
6+
const { Client } = pg;
7+
8+
// Railway PostgreSQL URL from environment
9+
const DATABASE_URL = process.env.DATABASE_URL ||
10+
'postgresql://postgres:YTSFXqPzFhghOcefgwPvJyWOBTYHbYxd@postgres.railway.internal:5432/railway';
11+
12+
async function testPostgreSQL() {
13+
console.log('🐘 Testing PostgreSQL Connection...\n');
14+
15+
const pgClient = new Client({
16+
connectionString: DATABASE_URL
17+
});
18+
19+
try {
20+
console.log('📡 Connecting to PostgreSQL...');
21+
await pgClient.connect();
22+
console.log('✅ Connected to PostgreSQL!\n');
23+
24+
// Test basic query
25+
const timeResult = await pgClient.query('SELECT NOW() as current_time');
26+
console.log('⏰ Database time:', timeResult.rows[0].current_time);
27+
28+
// Create frames table if it doesn't exist
29+
console.log('\n📊 Creating frames table...');
30+
await pgClient.query(`
31+
CREATE TABLE IF NOT EXISTS frames (
32+
frame_id TEXT PRIMARY KEY,
33+
run_id TEXT NOT NULL,
34+
project_id TEXT NOT NULL,
35+
parent_frame_id TEXT,
36+
depth INTEGER DEFAULT 0,
37+
type TEXT NOT NULL,
38+
name TEXT NOT NULL,
39+
state TEXT DEFAULT 'active',
40+
inputs JSONB DEFAULT '{}',
41+
outputs JSONB DEFAULT '{}',
42+
digest_text TEXT,
43+
digest_json JSONB DEFAULT '{}',
44+
created_at TIMESTAMP DEFAULT NOW(),
45+
closed_at TIMESTAMP
46+
);
47+
48+
CREATE INDEX IF NOT EXISTS idx_frames_project ON frames(project_id);
49+
CREATE INDEX IF NOT EXISTS idx_frames_state ON frames(state);
50+
CREATE INDEX IF NOT EXISTS idx_frames_created ON frames(created_at);
51+
`);
52+
console.log('✅ Frames table ready!\n');
53+
54+
// Check existing frames
55+
const countResult = await pgClient.query('SELECT COUNT(*) as count FROM frames');
56+
console.log('📈 Existing frames:', countResult.rows[0].count);
57+
58+
// Insert a test frame
59+
const testFrameId = `test-frame-${Date.now()}`;
60+
console.log('\n🔧 Inserting test frame...');
61+
await pgClient.query(`
62+
INSERT INTO frames (
63+
frame_id, run_id, project_id, type, name, state,
64+
inputs, outputs, digest_text
65+
) VALUES (
66+
$1, $2, $3, $4, $5, $6, $7, $8, $9
67+
)
68+
`, [
69+
testFrameId,
70+
'test-run-001',
71+
'stackmemory-test',
72+
'test',
73+
'Database Connection Test',
74+
'active',
75+
JSON.stringify({ test: true, timestamp: new Date().toISOString() }),
76+
JSON.stringify({ success: true }),
77+
'Test frame for Railway PostgreSQL connection'
78+
]);
79+
console.log('✅ Test frame inserted:', testFrameId);
80+
81+
// Retrieve the test frame
82+
console.log('\n🔍 Retrieving test frame...');
83+
const frameResult = await pgClient.query(
84+
'SELECT * FROM frames WHERE frame_id = $1',
85+
[testFrameId]
86+
);
87+
88+
if (frameResult.rows.length > 0) {
89+
const frame = frameResult.rows[0];
90+
console.log('✅ Frame retrieved successfully!');
91+
console.log(' - Name:', frame.name);
92+
console.log(' - Type:', frame.type);
93+
console.log(' - State:', frame.state);
94+
console.log(' - Created:', frame.created_at);
95+
}
96+
97+
// Get recent frames
98+
console.log('\n📋 Recent frames:');
99+
const recentFrames = await pgClient.query(`
100+
SELECT frame_id, name, type, state, created_at
101+
FROM frames
102+
ORDER BY created_at DESC
103+
LIMIT 5
104+
`);
105+
106+
if (recentFrames.rows.length > 0) {
107+
recentFrames.rows.forEach((frame, index) => {
108+
console.log(` ${index + 1}. ${frame.name} (${frame.type}) - ${frame.state}`);
109+
});
110+
} else {
111+
console.log(' No frames found');
112+
}
113+
114+
// Check table information
115+
console.log('\n📊 Database tables:');
116+
const tables = await pgClient.query(`
117+
SELECT table_name
118+
FROM information_schema.tables
119+
WHERE table_schema = 'public'
120+
ORDER BY table_name
121+
`);
122+
123+
tables.rows.forEach(row => {
124+
console.log(` - ${row.table_name}`);
125+
});
126+
127+
} catch (error) {
128+
console.error('❌ PostgreSQL Error:', error.message);
129+
if (error.message.includes('ENOTFOUND')) {
130+
console.log('\n💡 Note: postgres.railway.internal only works from within Railway');
131+
console.log(' For local testing, you need the external DATABASE_URL');
132+
}
133+
} finally {
134+
await pgClient.end();
135+
console.log('\n🔌 PostgreSQL connection closed');
136+
}
137+
}
138+
139+
async function testRedis() {
140+
console.log('\n\n🔴 Testing Redis Connection...\n');
141+
142+
// Try to build Redis URL from environment
143+
const REDIS_URL = process.env.REDIS_URL ||
144+
process.env.REDISHOST ? `redis://${process.env.REDISHOST}:${process.env.REDISPORT || 6379}` : null;
145+
146+
if (!REDIS_URL) {
147+
console.log('⚠️ No Redis configuration found in environment');
148+
console.log(' Add REDIS_URL or REDISHOST to Railway variables');
149+
return;
150+
}
151+
152+
const redisClient = createClient({ url: REDIS_URL });
153+
154+
try {
155+
console.log('📡 Connecting to Redis...');
156+
await redisClient.connect();
157+
console.log('✅ Connected to Redis!\n');
158+
159+
// Test basic operations
160+
console.log('🔧 Testing Redis operations...');
161+
162+
// Set a test key
163+
const testKey = `test:connection:${Date.now()}`;
164+
await redisClient.set(testKey, JSON.stringify({
165+
test: true,
166+
timestamp: new Date().toISOString(),
167+
message: 'Railway Redis connection test'
168+
}), { EX: 60 }); // Expire after 60 seconds
169+
console.log('✅ Set test key:', testKey);
170+
171+
// Get the test key
172+
const value = await redisClient.get(testKey);
173+
const parsed = JSON.parse(value);
174+
console.log('✅ Retrieved value:', parsed);
175+
176+
// Test Redis info
177+
const info = await redisClient.info('server');
178+
const version = info.match(/redis_version:(.+)/)?.[1];
179+
console.log('\n📊 Redis Server Info:');
180+
console.log(' - Version:', version);
181+
182+
// Check memory usage
183+
const memoryInfo = await redisClient.info('memory');
184+
const usedMemory = memoryInfo.match(/used_memory_human:(.+)/)?.[1];
185+
console.log(' - Memory used:', usedMemory);
186+
187+
// List all keys (be careful in production!)
188+
const keys = await redisClient.keys('*');
189+
console.log(' - Total keys:', keys.length);
190+
191+
if (keys.length > 0 && keys.length <= 10) {
192+
console.log(' - Keys:', keys);
193+
}
194+
195+
} catch (error) {
196+
console.error('❌ Redis Error:', error.message);
197+
if (error.message.includes('ENOTFOUND')) {
198+
console.log('\n💡 Note: Redis host not found');
199+
console.log(' Make sure Redis variables are configured in Railway');
200+
}
201+
} finally {
202+
await redisClient.disconnect();
203+
console.log('\n🔌 Redis connection closed');
204+
}
205+
}
206+
207+
// Run tests
208+
async function runTests() {
209+
console.log('🚀 Railway Database Connection Tests\n');
210+
console.log('=' .repeat(50));
211+
212+
await testPostgreSQL();
213+
await testRedis();
214+
215+
console.log('\n' + '=' .repeat(50));
216+
console.log('✅ Tests complete!\n');
217+
}
218+
219+
runTests().catch(error => {
220+
console.error('Fatal error:', error);
221+
process.exit(1);
222+
});

src/servers/railway/minimal.ts

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import http from 'http';
77

88
const PORT = process.env.PORT || 3000;
99

10-
const server = http.createServer((req, res) => {
10+
const server = http.createServer(async (req, res) => {
1111
console.log(`${new Date().toISOString()} ${req.method} ${req.url}`);
1212

1313
if (req.url === '/health' || req.url === '/api/health') {
@@ -20,12 +20,70 @@ const server = http.createServer((req, res) => {
2020
env: process.env.NODE_ENV || 'development',
2121
})
2222
);
23+
} else if (req.url === '/test-db') {
24+
res.writeHead(200, { 'Content-Type': 'application/json' });
25+
26+
// Test database connections
27+
const testResults = { postgresql: {}, redis: {} };
28+
29+
// Test PostgreSQL
30+
if (process.env.DATABASE_URL) {
31+
try {
32+
const { Client } = (await import('pg')).default;
33+
const pgClient = new Client({ connectionString: process.env.DATABASE_URL });
34+
await pgClient.connect();
35+
36+
const result = await pgClient.query('SELECT NOW() as time, version() as version');
37+
testResults.postgresql = {
38+
status: 'connected',
39+
time: result.rows[0].time,
40+
version: result.rows[0].version.split(' ')[0]
41+
};
42+
43+
await pgClient.end();
44+
} catch (error) {
45+
testResults.postgresql = { status: 'error', message: error.message };
46+
}
47+
} else {
48+
testResults.postgresql = { status: 'not_configured' };
49+
}
50+
51+
// Test Redis
52+
const redisUrl = process.env.REDIS_URL ||
53+
(process.env.REDISHOST ? `redis://${process.env.REDISHOST}:${process.env.REDISPORT || 6379}` : null);
54+
55+
if (redisUrl) {
56+
try {
57+
const { createClient } = await import('redis');
58+
const redisClient = createClient({ url: redisUrl });
59+
await redisClient.connect();
60+
61+
await redisClient.ping();
62+
const info = await redisClient.info('server');
63+
const version = info.match(/redis_version:(.+)/)?.[1];
64+
65+
testResults.redis = {
66+
status: 'connected',
67+
version: version,
68+
url: redisUrl.replace(/:\/\/[^@]+@/, '://***:***@') // Hide credentials
69+
};
70+
71+
await redisClient.disconnect();
72+
} catch (error) {
73+
testResults.redis = { status: 'error', message: error.message };
74+
}
75+
} else {
76+
testResults.redis = { status: 'not_configured' };
77+
}
78+
79+
res.end(JSON.stringify(testResults, null, 2));
2380
} else if (req.url === '/') {
2481
res.writeHead(200, { 'Content-Type': 'application/json' });
2582
res.end(
2683
JSON.stringify({
2784
message: 'StackMemory Minimal Server Running',
2885
version: '1.0.0',
86+
endpoints: ['/health', '/api/health', '/test-db']
2987
})
3088
);
3189
} else {

0 commit comments

Comments
 (0)