query-2jz
Version:
Query-2jz: A GraphQL alternative with faster performance and simpler use
371 lines (320 loc) • 9.77 kB
text/typescript
import { Request, Response, NextFunction, Router } from 'express';
import { Qryn, QrynConfig, QrynModel } from '../index';
/**
* Express.js Adapter for Qryn
*/
export class QrynExpressAdapter {
private qryn: Qryn;
private router: Router;
constructor(config: QrynConfig, models: QrynModel[] = []) {
this.qryn = new Qryn(config, models);
this.router = Router();
this.setupRoutes();
}
/**
* Initialize the adapter
*/
async initialize(): Promise<void> {
await this.qryn.initialize();
}
/**
* Get Express router
*/
getRouter(): Router {
return this.router;
}
/**
* Get Qryn instance
*/
getQryn(): Qryn {
return this.qryn;
}
/**
* Setup Express routes
*/
private setupRoutes(): void {
// Health check
this.router.get('/health', this.healthCheck.bind(this));
// Query endpoint
this.router.get('/api/qryn/:model', this.handleQuery.bind(this));
// Mutation endpoint
this.router.post('/api/qryn/:model', this.handleMutation.bind(this));
// Real-time subscription endpoint
this.router.get('/api/qryn/:model/subscribe', this.handleSubscription.bind(this));
// Type generation endpoint
this.router.post('/api/qryn/generate-types', this.handleTypeGeneration.bind(this));
// Statistics endpoint
this.router.get('/api/qryn/stats', this.handleStats.bind(this));
// Backup endpoints
this.router.post('/api/qryn/backup/create', this.handleCreateBackup.bind(this));
this.router.post('/api/qryn/backup/restore', this.handleRestoreBackup.bind(this));
this.router.get('/api/qryn/backup/list', this.handleListBackups.bind(this));
// Middleware for error handling
this.router.use(this.errorHandler.bind(this));
}
/**
* Health check endpoint
*/
private async healthCheck(req: Request, res: Response): Promise<void> {
try {
const stats = this.qryn.getStats();
res.json({
status: 'healthy',
timestamp: new Date().toISOString(),
...stats
});
} catch (error) {
res.status(500).json({
status: 'unhealthy',
error: error instanceof Error ? error.message : 'Unknown error'
});
}
}
/**
* Handle query requests
*/
private async handleQuery(req: Request, res: Response): Promise<void> {
try {
const { model } = req.params;
const query = this.parseQueryParams(req.query);
// Add request context
const context = this.createRequestContext(req);
const result = await this.qryn.query(query, model);
// Set cache headers
if (result.meta?.etag) {
res.set('ETag', result.meta.etag);
}
if (result.meta?.lastModified) {
res.set('Last-Modified', result.meta.lastModified);
}
res.set('Cache-Control', 'public, max-age=300');
res.json(result);
} catch (error) {
res.status(500).json({
error: 'Query execution failed',
message: error instanceof Error ? error.message : 'Unknown error'
});
}
}
/**
* Handle mutation requests
*/
private async handleMutation(req: Request, res: Response): Promise<void> {
try {
const { model } = req.params;
const mutation = req.body;
// Validate mutation
if (!mutation.operation) {
res.status(400).json({
error: 'Invalid mutation',
message: 'Operation is required'
});
return;
}
const result = await this.qryn.mutate(mutation, model);
res.set('Cache-Control', 'no-cache');
res.json(result);
} catch (error) {
res.status(500).json({
error: 'Mutation execution failed',
message: error instanceof Error ? error.message : 'Unknown error'
});
}
}
/**
* Handle real-time subscription requests
*/
private async handleSubscription(req: Request, res: Response): Promise<void> {
try {
const { model } = req.params;
const query = this.parseQueryParams(req.query);
// Set SSE headers
res.writeHead(200, {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Headers': 'Cache-Control'
});
// Create subscription
const subscriptionId = this.qryn.subscribe(query, model, (data) => {
res.write(`data: ${JSON.stringify(data)}\n\n`);
});
// Handle client disconnect
req.on('close', () => {
if (subscriptionId) {
this.qryn.unsubscribe(subscriptionId);
}
});
// Send initial connection message
res.write(`data: ${JSON.stringify({
type: 'connected',
subscriptionId
})}\n\n`);
} catch (error) {
res.status(500).json({
error: 'Subscription failed',
message: error instanceof Error ? error.message : 'Unknown error'
});
}
}
/**
* Handle type generation requests
*/
private async handleTypeGeneration(req: Request, res: Response): Promise<void> {
try {
const { outputDir } = req.body;
this.qryn.generateTypes(outputDir);
res.json({
message: 'Types generated successfully',
outputDir: outputDir || './generated'
});
} catch (error) {
res.status(500).json({
error: 'Type generation failed',
message: error instanceof Error ? error.message : 'Unknown error'
});
}
}
/**
* Handle statistics requests
*/
private async handleStats(req: Request, res: Response): Promise<void> {
try {
const stats = this.qryn.getStats();
res.json(stats);
} catch (error) {
res.status(500).json({
error: 'Failed to get statistics',
message: error instanceof Error ? error.message : 'Unknown error'
});
}
}
/**
* Handle backup creation
*/
private async handleCreateBackup(req: Request, res: Response): Promise<void> {
try {
const { outputPath, includeSchema, includeData, compress, filter } = req.body;
// This would use the backup system
res.json({
message: 'Backup created successfully',
path: outputPath
});
} catch (error) {
res.status(500).json({
error: 'Backup creation failed',
message: error instanceof Error ? error.message : 'Unknown error'
});
}
}
/**
* Handle backup restoration
*/
private async handleRestoreBackup(req: Request, res: Response): Promise<void> {
try {
const { backupPath, includeSchema, includeData, mode } = req.body;
// This would use the backup system
res.json({
message: 'Backup restored successfully'
});
} catch (error) {
res.status(500).json({
error: 'Backup restoration failed',
message: error instanceof Error ? error.message : 'Unknown error'
});
}
}
/**
* Handle backup listing
*/
private async handleListBackups(req: Request, res: Response): Promise<void> {
try {
// This would list available backups
res.json({
backups: []
});
} catch (error) {
res.status(500).json({
error: 'Failed to list backups',
message: error instanceof Error ? error.message : 'Unknown error'
});
}
}
/**
* Error handler middleware
*/
private errorHandler(error: any, req: Request, res: Response, next: NextFunction): void {
console.error('Qryn Express Adapter Error:', error);
res.status(500).json({
error: 'Internal server error',
message: error instanceof Error ? error.message : 'Unknown error',
timestamp: new Date().toISOString()
});
}
/**
* Parse query parameters into Qryn query
*/
private parseQueryParams(queryParams: any): any {
const query: any = {};
if (queryParams.select) {
try {
query.select = JSON.parse(queryParams.select);
} catch {
query.select = {};
}
}
if (queryParams.where) {
try {
query.where = JSON.parse(queryParams.where);
} catch {
query.where = {};
}
}
if (queryParams.orderBy) {
try {
query.orderBy = JSON.parse(queryParams.orderBy);
} catch {
query.orderBy = {};
}
}
if (queryParams.limit) {
query.limit = parseInt(queryParams.limit);
}
if (queryParams.offset) {
query.offset = parseInt(queryParams.offset);
}
if (queryParams.live === 'true') {
query.live = true;
}
return query;
}
/**
* Create request context
*/
private createRequestContext(req: Request): any {
return {
userId: req.headers['x-user-id'] as string || 'anonymous',
tenantId: req.headers['x-tenant-id'] as string,
requestId: req.headers['x-request-id'] as string,
userAgent: req.headers['user-agent'],
ip: req.ip,
metadata: {
method: req.method,
url: req.url,
timestamp: new Date().toISOString()
}
};
}
}
/**
* Express middleware factory
*/
export function createQrynMiddleware(config: QrynConfig, models: QrynModel[] = []) {
const adapter = new QrynExpressAdapter(config, models);
return {
adapter,
middleware: adapter.getRouter(),
initialize: () => adapter.initialize()
};
}