dsandsl
Version:
Data Service AND Security Layer - Universal role-based data filtering and security for Node.js applications
339 lines (287 loc) • 10.7 kB
JavaScript
/**
* @fileoverview Express.js Framework Adapter
* Provides middleware and helpers for Express applications
*/
const { DSLError, FrameworkError, validateRequired, validateTypes } = require('../core/DSLErrors')
const debug = require('debug')('dsandsl:express')
/**
* Express.js adapter for DSANDSL
*/
class ExpressAdapter {
/**
* Create Express middleware that attaches DSL filtering to requests
* @param {DSLEngine} dsl - DSL engine instance
* @param {Object} options - Middleware options
* @returns {Function} Express middleware function
*/
static middleware(dsl, options = {}) {
try {
// Validate inputs
validateRequired({ dsl }, ['dsl'])
validateTypes({ dsl }, { dsl: 'object' })
const config = {
roleExtractor: options.roleExtractor || ((req) => req.user?.role || 'guest'),
attachTo: options.attachTo || 'dsl',
autoFilter: options.autoFilter !== false,
errorHandler: options.errorHandler || this.defaultErrorHandler,
contextExtractor: options.contextExtractor || (() => ({})),
skipPaths: options.skipPaths || [],
...options
}
debug('Express middleware configured:', {
attachTo: config.attachTo,
autoFilter: config.autoFilter,
skipPaths: config.skipPaths.length
})
return function dslMiddleware(req, res, next) {
try {
// Check if path should be skipped
if (config.skipPaths.some(path => req.path.startsWith(path))) {
return next()
}
// Extract user role
const userRole = typeof config.roleExtractor === 'function'
? config.roleExtractor(req, res)
: config.roleExtractor
// Extract additional context
const context = config.contextExtractor(req, res)
// Create DSL helper object
const dslHelper = {
// Core filtering function
filter: (data, options = {}) => {
return dsl.filter(data, userRole, { ...context, ...options })
},
// Check field access
checkAccess: (fieldName, customRole = null) => {
return dsl.checkAccess(fieldName, customRole || userRole, context)
},
// Get allowed fields
getAllowedFields: (category = null, customRole = null) => {
return dsl.getAllowedFields(customRole || userRole, category)
},
// Role information
userRole,
context,
// Response helpers
json: (data, options = {}) => {
const filtered = dsl.filter(data, userRole, { ...context, ...options })
return res.json(filtered)
},
jsonWithMetadata: (data, options = {}) => {
const result = dsl.filter(data, userRole, {
includeMetadata: true,
...context,
...options
})
return res.json(result)
},
// Error response
accessDenied: (message = 'Access denied', field = null) => {
const error = {
error: 'ACCESS_DENIED',
message,
userRole,
...(field && { field }),
timestamp: new Date().toISOString()
}
return res.status(403).json(error)
}
}
// Attach DSL helper to request
req[config.attachTo] = dslHelper
debug('DSL attached to request:', {
path: req.path,
method: req.method,
userRole,
attachedTo: config.attachTo
})
next()
} catch (error) {
debug('Middleware error:', error.message)
config.errorHandler(error, req, res, next)
}
}
} catch (error) {
throw new FrameworkError(
`Failed to create Express middleware: ${error.message}`,
'express',
'middleware_creation'
)
}
}
/**
* Create a route handler with automatic DSL filtering
* @param {DSLEngine} dsl - DSL engine instance
* @param {Function} handler - Route handler function
* @param {Object} options - Route options
* @returns {Function} Wrapped route handler
*/
static createRoute(dsl, handler, options = {}) {
try {
validateRequired({ dsl, handler }, ['dsl', 'handler'])
validateTypes({ handler }, { handler: 'function' })
const config = {
roleExtractor: options.roleExtractor || ((req) => req.user?.role || 'guest'),
errorHandler: options.errorHandler || this.defaultErrorHandler,
autoFilter: options.autoFilter !== false,
...options
}
return async function dslRouteHandler(req, res, next) {
try {
const userRole = config.roleExtractor(req, res)
// Create DSL context for this request
const dslContext = {
filter: (data, filterOptions = {}) => {
return dsl.filter(data, userRole, filterOptions)
},
checkAccess: (fieldName) => {
return dsl.checkAccess(fieldName, userRole)
},
userRole
}
// Attach to request
req.dsl = dslContext
// Call original handler
const result = await handler(req, res, next)
// If handler returns data and autoFilter is enabled, filter it
if (config.autoFilter && result && typeof result === 'object') {
const filtered = dsl.filter(result, userRole)
return res.json(filtered)
}
return result
} catch (error) {
debug('Route handler error:', error.message)
config.errorHandler(error, req, res, next)
}
}
} catch (error) {
throw new FrameworkError(
`Failed to create Express route: ${error.message}`,
'express',
'route_creation'
)
}
}
/**
* Create route-specific middleware for different endpoints
* @param {DSLEngine} dsl - DSL engine instance
* @param {Object} routes - Route configuration object
* @returns {Object} Object with route-specific middleware
*/
static createRouteMiddleware(dsl, routes) {
const routeMiddleware = {}
Object.entries(routes).forEach(([routeName, routeConfig]) => {
routeMiddleware[routeName] = this.middleware(dsl, routeConfig)
})
return routeMiddleware
}
/**
* Create error handling middleware
* @param {Object} options - Error handling options
* @returns {Function} Express error handling middleware
*/
static errorMiddleware(options = {}) {
const config = {
includeStack: options.includeStack || process.env.NODE_ENV === 'development',
logger: options.logger || console.error,
...options
}
return function dslErrorMiddleware(error, req, res, next) {
// Log the error
config.logger('DSL Error:', {
message: error.message,
code: error.code,
path: req.path,
method: req.method,
userRole: req.dsl?.userRole,
stack: config.includeStack ? error.stack : undefined
})
// Handle DSL-specific errors
if (error instanceof DSLError) {
const status = error.code === 'ACCESS_DENIED' ? 403 : 400
return res.status(status).json({
error: error.code || 'DSL_ERROR',
message: error.message,
...(config.includeStack && { stack: error.stack }),
timestamp: new Date().toISOString()
})
}
// Pass non-DSL errors to next error handler
next(error)
}
}
/**
* Default error handler for DSL middleware
* @param {Error} error - Error that occurred
* @param {Object} req - Express request object
* @param {Object} res - Express response object
* @param {Function} next - Express next function
*/
static defaultErrorHandler(error, req, res, next) {
debug('Default error handler:', error.message)
if (error instanceof DSLError) {
const status = error.code === 'ACCESS_DENIED' ? 403 : 500
return res.status(status).json({
error: 'DSL_ERROR',
message: error.message,
code: error.code
})
}
// Pass unknown errors to Express
next(error)
}
/**
* Create role-based route protection middleware
* @param {Array<string>} allowedRoles - Array of allowed roles
* @param {Object} options - Protection options
* @returns {Function} Express middleware
*/
static requireRoles(allowedRoles, options = {}) {
const config = {
roleExtractor: options.roleExtractor || ((req) => req.user?.role || 'guest'),
errorHandler: options.errorHandler || this.defaultErrorHandler,
...options
}
return function roleProtectionMiddleware(req, res, next) {
try {
const userRole = config.roleExtractor(req, res)
if (!allowedRoles.includes(userRole)) {
const error = new DSLError(
`Access denied. Required roles: ${allowedRoles.join(', ')}. User role: ${userRole}`,
'ACCESS_DENIED',
{ requiredRoles: allowedRoles, userRole }
)
return config.errorHandler(error, req, res, next)
}
next()
} catch (error) {
config.errorHandler(error, req, res, next)
}
}
}
/**
* Validate Express adapter configuration
* @param {Object} config - Configuration to validate
* @throws {FrameworkError} If configuration is invalid
*/
static validateConfig(config) {
const errors = []
if (config.roleExtractor && typeof config.roleExtractor !== 'function') {
errors.push('roleExtractor must be a function')
}
if (config.errorHandler && typeof config.errorHandler !== 'function') {
errors.push('errorHandler must be a function')
}
if (config.skipPaths && !Array.isArray(config.skipPaths)) {
errors.push('skipPaths must be an array')
}
if (errors.length > 0) {
throw new FrameworkError(
`Invalid Express adapter configuration: ${errors.join(', ')}`,
'express',
'configuration_validation'
)
}
}
}
module.exports = ExpressAdapter