264 lines
18 KiB
JavaScript
264 lines
18 KiB
JavaScript
import { supabase } from '../commons/supabase.js';
|
|
import { logger } from '../commons/logger.js';
|
|
import { FunctionRegistry } from '../commons/registry.js';
|
|
/**
|
|
* Middleware to track API usage for billing and monitoring
|
|
* Tracks request start and updates with completion status
|
|
*/
|
|
export async function usageTrackingMiddleware(c, next) {
|
|
const startTime = Date.now();
|
|
// Extract user ID from context (set by auth middleware)
|
|
const userId = c.get('userId');
|
|
// Skip tracking for unauthenticated requests
|
|
if (!userId) {
|
|
logger.trace('[UsageTracking] Skipping - No userId');
|
|
await next();
|
|
return;
|
|
}
|
|
// Determine product and action
|
|
const path = c.req.path;
|
|
const method = c.req.method;
|
|
// Use Registry to find config
|
|
const config = FunctionRegistry.findByRoute(path, method);
|
|
const product = config?.productId;
|
|
const action = config?.actionId;
|
|
logger.trace(`[UsageTracking] Identified: product=${product}, action=${action}`);
|
|
// Skip if not a tracked endpoint
|
|
if (!product || !action || !config) {
|
|
logger.info('[UsageTracking] Skipping - Not a tracked endpoint');
|
|
await next();
|
|
return;
|
|
}
|
|
// Generate a job ID for this request
|
|
const jobId = `${product}_${action}_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
// Create initial usage record with 'processing' status
|
|
let usageId = null;
|
|
try {
|
|
const { data, error } = await supabase
|
|
.from('api_usage')
|
|
.insert({
|
|
user_id: userId,
|
|
endpoint: path,
|
|
method,
|
|
product,
|
|
action,
|
|
status: 'processing',
|
|
job_id: jobId,
|
|
cancellable: config.cancellable || false,
|
|
cost_units: config.costUnits,
|
|
metadata: {
|
|
query: c.req.query(),
|
|
userAgent: c.req.header('user-agent'),
|
|
ip: c.req.header('x-forwarded-for') || c.req.header('x-real-ip'),
|
|
},
|
|
})
|
|
.select('id')
|
|
.single();
|
|
if (error) {
|
|
logger.error({ err: error }, '[UsageTracking] Error creating usage record');
|
|
}
|
|
else if (data) {
|
|
logger.trace(`[UsageTracking] Created usage record: ${data.id}`);
|
|
usageId = data.id;
|
|
// Store usage ID in context for potential use in handlers
|
|
c.set('usageId', usageId);
|
|
c.set('jobId', jobId);
|
|
}
|
|
else {
|
|
logger.trace('[UsageTracking] No data returned from insert');
|
|
}
|
|
}
|
|
catch (err) {
|
|
logger.error({ err }, 'Failed to create usage record');
|
|
}
|
|
// Execute the request
|
|
let requestError = null;
|
|
try {
|
|
await next();
|
|
}
|
|
catch (err) {
|
|
requestError = err;
|
|
throw err; // Re-throw to let error handler deal with it
|
|
}
|
|
finally {
|
|
// Update usage record with completion status
|
|
const endTime = Date.now();
|
|
const responseTime = endTime - startTime;
|
|
if (usageId) {
|
|
// Check if handler requested to skip status update (e.g. for background jobs)
|
|
const skipUpdate = c.get('skipUsageStatusUpdate');
|
|
if (!skipUpdate) {
|
|
updateUsageRecord({
|
|
usageId,
|
|
responseStatus: c.res.status,
|
|
responseTimeMs: responseTime,
|
|
error: requestError,
|
|
}).catch(err => {
|
|
logger.error({ err }, 'Failed to update usage record');
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
/**
|
|
* Update usage record with completion status
|
|
*/
|
|
export async function updateUsageRecord(data) {
|
|
const status = data.error
|
|
? 'failed'
|
|
: (data.responseStatus >= 200 && data.responseStatus < 300)
|
|
? 'completed'
|
|
: 'failed';
|
|
const updateData = {
|
|
status,
|
|
response_status: data.responseStatus,
|
|
response_time_ms: data.responseTimeMs,
|
|
};
|
|
if (data.error) {
|
|
updateData.error_message = data.error.message;
|
|
}
|
|
const { error } = await supabase
|
|
.from('api_usage')
|
|
.update(updateData)
|
|
.eq('id', data.usageId);
|
|
if (error) {
|
|
logger.error({ err: error }, 'Error updating usage record');
|
|
}
|
|
}
|
|
/**
|
|
* Helper function to manually track usage (for non-middleware scenarios)
|
|
*/
|
|
export async function trackUsage(data) {
|
|
try {
|
|
const { data: record, error } = await supabase
|
|
.from('api_usage')
|
|
.insert({
|
|
user_id: data.userId,
|
|
endpoint: data.endpoint,
|
|
method: data.method,
|
|
product: data.product,
|
|
action: data.action,
|
|
status: data.responseStatus ? 'completed' : 'processing',
|
|
job_id: data.jobId,
|
|
cancellable: data.cancellable,
|
|
response_status: data.responseStatus,
|
|
response_time_ms: data.responseTimeMs,
|
|
cost_units: data.costUnits,
|
|
metadata: data.metadata,
|
|
api_key_id: data.apiKeyId,
|
|
})
|
|
.select('id')
|
|
.single();
|
|
if (error) {
|
|
logger.error({ err: error }, 'Error tracking usage');
|
|
return null;
|
|
}
|
|
return record?.id || null;
|
|
}
|
|
catch (err) {
|
|
logger.error({ err }, 'Failed to track usage');
|
|
return null;
|
|
}
|
|
}
|
|
/**
|
|
* Cancel a job by job ID
|
|
*/
|
|
export async function cancelJob(userId, jobId) {
|
|
try {
|
|
const { data, error } = await supabase
|
|
.from('api_usage')
|
|
.update({
|
|
status: 'cancelled',
|
|
})
|
|
.eq('user_id', userId)
|
|
.eq('job_id', jobId)
|
|
.eq('cancellable', true)
|
|
.in('status', ['pending', 'processing'])
|
|
.select('id');
|
|
if (error) {
|
|
logger.error({ err: error }, 'Error cancelling job');
|
|
return false;
|
|
}
|
|
return !!data && data.length > 0;
|
|
}
|
|
catch (err) {
|
|
logger.error({ err }, 'Failed to cancel job');
|
|
return false;
|
|
}
|
|
}
|
|
/**
|
|
* Get active (cancellable) jobs for a user
|
|
*/
|
|
export async function getActiveJobs(userId) {
|
|
try {
|
|
const { data, error } = await supabase
|
|
.from('api_usage')
|
|
.select('id, job_id, product, action, status, created_at, metadata')
|
|
.eq('user_id', userId)
|
|
.eq('cancellable', true)
|
|
.in('status', ['pending', 'processing'])
|
|
.order('created_at', { ascending: false });
|
|
if (error) {
|
|
logger.error({ err: error }, 'Error fetching active jobs');
|
|
return [];
|
|
}
|
|
return data || [];
|
|
}
|
|
catch (err) {
|
|
logger.error({ err }, 'Failed to fetch active jobs');
|
|
return [];
|
|
}
|
|
}
|
|
/**
|
|
* Pause a job by job ID
|
|
*/
|
|
export async function pauseJob(userId, jobId) {
|
|
try {
|
|
const { data, error } = await supabase
|
|
.from('api_usage')
|
|
.update({
|
|
status: 'paused',
|
|
})
|
|
.eq('user_id', userId)
|
|
.eq('job_id', jobId)
|
|
.eq('cancellable', true)
|
|
.eq('status', 'processing') // Only processing jobs can be paused
|
|
.select('id');
|
|
if (error) {
|
|
logger.error({ err: error }, 'Error pausing job');
|
|
return false;
|
|
}
|
|
return !!data && data.length > 0;
|
|
}
|
|
catch (err) {
|
|
logger.error({ err }, 'Failed to pause job');
|
|
return false;
|
|
}
|
|
}
|
|
/**
|
|
* Resume a paused job by job ID
|
|
*/
|
|
export async function resumeJob(userId, jobId) {
|
|
try {
|
|
const { data, error } = await supabase
|
|
.from('api_usage')
|
|
.update({
|
|
status: 'processing',
|
|
})
|
|
.eq('user_id', userId)
|
|
.eq('job_id', jobId)
|
|
.eq('cancellable', true)
|
|
.eq('status', 'paused') // Only paused jobs can be resumed
|
|
.select('id');
|
|
if (error) {
|
|
logger.error({ err: error }, 'Error resuming job');
|
|
return false;
|
|
}
|
|
return !!data && data.length > 0;
|
|
}
|
|
catch (err) {
|
|
logger.error({ err }, 'Failed to resume job');
|
|
return false;
|
|
}
|
|
}
|
|
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoidXNhZ2VUcmFja2luZy5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uL3NyYy9taWRkbGV3YXJlL3VzYWdlVHJhY2tpbmcudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQ0EsT0FBTyxFQUFFLFFBQVEsRUFBRSxNQUFNLHdCQUF3QixDQUFDO0FBQ2xELE9BQU8sRUFBRSxNQUFNLEVBQUUsTUFBTSxzQkFBc0IsQ0FBQztBQUM5QyxPQUFPLEVBQUUsZ0JBQWdCLEVBQUUsTUFBTSx3QkFBd0IsQ0FBQztBQWlCMUQ7OztHQUdHO0FBQ0gsTUFBTSxDQUFDLEtBQUssVUFBVSx1QkFBdUIsQ0FBQyxDQUFVLEVBQUUsSUFBVTtJQUNoRSxNQUFNLFNBQVMsR0FBRyxJQUFJLENBQUMsR0FBRyxFQUFFLENBQUM7SUFFN0Isd0RBQXdEO0lBQ3hELE1BQU0sTUFBTSxHQUFHLENBQUMsQ0FBQyxHQUFHLENBQUMsUUFBUSxDQUFDLENBQUM7SUFDL0IsNkNBQTZDO0lBQzdDLElBQUksQ0FBQyxNQUFNLEVBQUUsQ0FBQztRQUNWLE1BQU0sQ0FBQyxLQUFLLENBQUMsc0NBQXNDLENBQUMsQ0FBQztRQUNyRCxNQUFNLElBQUksRUFBRSxDQUFDO1FBQ2IsT0FBTztJQUNYLENBQUM7SUFFRCwrQkFBK0I7SUFDL0IsTUFBTSxJQUFJLEdBQUcsQ0FBQyxDQUFDLEdBQUcsQ0FBQyxJQUFJLENBQUM7SUFDeEIsTUFBTSxNQUFNLEdBQUcsQ0FBQyxDQUFDLEdBQUcsQ0FBQyxNQUFNLENBQUM7SUFFNUIsOEJBQThCO0lBQzlCLE1BQU0sTUFBTSxHQUFHLGdCQUFnQixDQUFDLFdBQVcsQ0FBQyxJQUFJLEVBQUUsTUFBTSxDQUFDLENBQUM7SUFDMUQsTUFBTSxPQUFPLEdBQUcsTUFBTSxFQUFFLFNBQVMsQ0FBQztJQUNsQyxNQUFNLE1BQU0sR0FBRyxNQUFNLEVBQUUsUUFBUSxDQUFDO0lBRWhDLE1BQU0sQ0FBQyxLQUFLLENBQUMsdUNBQXVDLE9BQU8sWUFBWSxNQUFNLEVB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
|