- Add comprehensive database migrations (001-024) for schema evolution - Enhance API schema with expanded type definitions and resolvers - Add new middleware: audit logging, rate limiting, MFA enforcement, security, tenant auth - Implement new services: AI optimization, billing, blockchain, compliance, marketplace - Add adapter layer for cloud integrations (Cloudflare, Kubernetes, Proxmox, storage) - Update Crossplane provider with enhanced VM management capabilities - Add comprehensive test suite for API endpoints and services - Update frontend components with improved GraphQL subscriptions and real-time updates - Enhance security configurations and headers (CSP, CORS, etc.) - Update documentation and configuration files - Add new CI/CD workflows and validation scripts - Implement design system improvements and UI enhancements
449 lines
12 KiB
TypeScript
449 lines
12 KiB
TypeScript
import { getDb } from '../db'
|
|
import { Context } from '../types/context'
|
|
import { GraphQLError } from 'graphql'
|
|
import { logger } from '../lib/logger'
|
|
import crypto from 'crypto'
|
|
|
|
export interface APIMarketplaceListing {
|
|
id: string
|
|
name: string
|
|
description: string
|
|
provider: string
|
|
endpoint: string
|
|
documentationUrl: string | null
|
|
category: string
|
|
pricing: {
|
|
model: string
|
|
basePrice: number | null
|
|
perRequestPrice: number | null
|
|
freeTier: {
|
|
requestsPerMonth: number
|
|
features: string[]
|
|
} | null
|
|
}
|
|
rating: number
|
|
reviewCount: number
|
|
requestCount: number
|
|
status: string
|
|
createdAt: Date
|
|
updatedAt: Date
|
|
}
|
|
|
|
export interface APISubscription {
|
|
id: string
|
|
listingId: string
|
|
userId: string
|
|
status: string
|
|
apiKey: string | null
|
|
createdAt: Date
|
|
updatedAt: Date
|
|
}
|
|
|
|
export async function getAPIMarketplaceListings(filter?: {
|
|
category?: string
|
|
search?: string
|
|
status?: string
|
|
limit?: number
|
|
offset?: number
|
|
}): Promise<APIMarketplaceListing[]> {
|
|
const db = getDb()
|
|
const conditions: string[] = []
|
|
const params: any[] = []
|
|
let paramIndex = 1
|
|
|
|
if (filter?.category) {
|
|
conditions.push(`category = $${paramIndex++}`)
|
|
params.push(filter.category)
|
|
}
|
|
|
|
if (filter?.search) {
|
|
conditions.push(`(name ILIKE $${paramIndex} OR description ILIKE $${paramIndex})`)
|
|
params.push(`%${filter.search}%`)
|
|
paramIndex++
|
|
}
|
|
|
|
if (filter?.status) {
|
|
conditions.push(`status = $${paramIndex++}`)
|
|
params.push(filter.status)
|
|
}
|
|
|
|
const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : ''
|
|
const limit = filter?.limit || 50
|
|
const offset = filter?.offset || 0
|
|
|
|
const result = await db.query(`
|
|
SELECT
|
|
l.*,
|
|
COALESCE(AVG(r.rating), 0) as rating,
|
|
COUNT(DISTINCT r.id) as review_count,
|
|
COUNT(DISTINCT s.id) as request_count
|
|
FROM api_marketplace_listings l
|
|
LEFT JOIN api_marketplace_reviews r ON r.listing_id = l.id
|
|
LEFT JOIN api_subscriptions s ON s.listing_id = l.id
|
|
${whereClause}
|
|
GROUP BY l.id
|
|
ORDER BY l.created_at DESC
|
|
LIMIT $${paramIndex++} OFFSET $${paramIndex++}
|
|
`, [...params, limit, offset])
|
|
|
|
return result.rows.map(row => ({
|
|
id: row.id,
|
|
name: row.name,
|
|
description: row.description,
|
|
provider: row.provider,
|
|
endpoint: row.endpoint,
|
|
documentationUrl: row.documentation_url,
|
|
category: row.category,
|
|
pricing: {
|
|
model: row.pricing_model,
|
|
basePrice: row.base_price,
|
|
perRequestPrice: row.per_request_price,
|
|
freeTier: row.free_tier ? JSON.parse(row.free_tier) : null,
|
|
},
|
|
rating: parseFloat(row.rating) || 0,
|
|
reviewCount: parseInt(row.review_count) || 0,
|
|
requestCount: parseInt(row.request_count) || 0,
|
|
status: row.status,
|
|
createdAt: new Date(row.created_at),
|
|
updatedAt: new Date(row.updated_at),
|
|
}))
|
|
}
|
|
|
|
export async function getAPIMarketplaceListing(id: string): Promise<APIMarketplaceListing | null> {
|
|
const db = getDb()
|
|
const result = await db.query(`
|
|
SELECT
|
|
l.*,
|
|
COALESCE(AVG(r.rating), 0) as rating,
|
|
COUNT(DISTINCT r.id) as review_count,
|
|
COUNT(DISTINCT s.id) as request_count
|
|
FROM api_marketplace_listings l
|
|
LEFT JOIN api_marketplace_reviews r ON r.listing_id = l.id
|
|
LEFT JOIN api_subscriptions s ON s.listing_id = l.id
|
|
WHERE l.id = $1
|
|
GROUP BY l.id
|
|
`, [id])
|
|
|
|
if (result.rows.length === 0) {
|
|
return null
|
|
}
|
|
|
|
const row = result.rows[0]
|
|
return {
|
|
id: row.id,
|
|
name: row.name,
|
|
description: row.description,
|
|
provider: row.provider,
|
|
endpoint: row.endpoint,
|
|
documentationUrl: row.documentation_url,
|
|
category: row.category,
|
|
pricing: {
|
|
model: row.pricing_model,
|
|
basePrice: row.base_price,
|
|
perRequestPrice: row.per_request_price,
|
|
freeTier: row.free_tier ? JSON.parse(row.free_tier) : null,
|
|
},
|
|
rating: parseFloat(row.rating) || 0,
|
|
reviewCount: parseInt(row.review_count) || 0,
|
|
requestCount: parseInt(row.request_count) || 0,
|
|
status: row.status,
|
|
createdAt: new Date(row.created_at),
|
|
updatedAt: new Date(row.updated_at),
|
|
}
|
|
}
|
|
|
|
export async function getMyAPISubscriptions(context: Context): Promise<APISubscription[]> {
|
|
if (!context.user) {
|
|
throw new GraphQLError('Authentication required', {
|
|
extensions: { code: 'UNAUTHENTICATED' },
|
|
})
|
|
}
|
|
|
|
const db = getDb()
|
|
const result = await db.query(
|
|
'SELECT * FROM api_subscriptions WHERE user_id = $1 ORDER BY created_at DESC',
|
|
[context.user.id]
|
|
)
|
|
|
|
return result.rows.map(row => ({
|
|
id: row.id,
|
|
listingId: row.listing_id,
|
|
userId: row.user_id,
|
|
status: row.status,
|
|
apiKey: row.api_key,
|
|
createdAt: new Date(row.created_at),
|
|
updatedAt: new Date(row.updated_at),
|
|
}))
|
|
}
|
|
|
|
export async function createAPIMarketplaceListing(
|
|
context: Context,
|
|
input: {
|
|
name: string
|
|
description: string
|
|
provider: string
|
|
endpoint: string
|
|
documentationUrl?: string
|
|
category: string
|
|
pricing: {
|
|
model: string
|
|
basePrice?: number
|
|
perRequestPrice?: number
|
|
freeTier?: {
|
|
requestsPerMonth: number
|
|
features: string[]
|
|
}
|
|
}
|
|
}
|
|
): Promise<APIMarketplaceListing> {
|
|
if (!context.user) {
|
|
throw new GraphQLError('Authentication required', {
|
|
extensions: { code: 'UNAUTHENTICATED' },
|
|
})
|
|
}
|
|
|
|
const db = getDb()
|
|
const result = await db.query(
|
|
`INSERT INTO api_marketplace_listings
|
|
(name, description, provider, endpoint, documentation_url, category, pricing_model, base_price, per_request_price, free_tier, status, created_at, updated_at)
|
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, 'PENDING', NOW(), NOW())
|
|
RETURNING *`,
|
|
[
|
|
input.name,
|
|
input.description,
|
|
input.provider,
|
|
input.endpoint,
|
|
input.documentationUrl || null,
|
|
input.category,
|
|
input.pricing.model,
|
|
input.pricing.basePrice || null,
|
|
input.pricing.perRequestPrice || null,
|
|
input.pricing.freeTier ? JSON.stringify(input.pricing.freeTier) : null,
|
|
]
|
|
)
|
|
|
|
const row = result.rows[0]
|
|
return {
|
|
id: row.id,
|
|
name: row.name,
|
|
description: row.description,
|
|
provider: row.provider,
|
|
endpoint: row.endpoint,
|
|
documentationUrl: row.documentation_url,
|
|
category: row.category,
|
|
pricing: {
|
|
model: row.pricing_model,
|
|
basePrice: row.base_price,
|
|
perRequestPrice: row.per_request_price,
|
|
freeTier: row.free_tier ? JSON.parse(row.free_tier) : null,
|
|
},
|
|
rating: 0,
|
|
reviewCount: 0,
|
|
requestCount: 0,
|
|
status: row.status,
|
|
createdAt: new Date(row.created_at),
|
|
updatedAt: new Date(row.updated_at),
|
|
}
|
|
}
|
|
|
|
export async function updateAPIMarketplaceListing(
|
|
context: Context,
|
|
id: string,
|
|
input: {
|
|
name?: string
|
|
description?: string
|
|
status?: string
|
|
pricing?: {
|
|
model: string
|
|
basePrice?: number
|
|
perRequestPrice?: number
|
|
freeTier?: {
|
|
requestsPerMonth: number
|
|
features: string[]
|
|
}
|
|
}
|
|
}
|
|
): Promise<APIMarketplaceListing> {
|
|
if (!context.user) {
|
|
throw new GraphQLError('Authentication required', {
|
|
extensions: { code: 'UNAUTHENTICATED' },
|
|
})
|
|
}
|
|
|
|
const db = getDb()
|
|
const updates: string[] = []
|
|
const params: any[] = []
|
|
let paramIndex = 1
|
|
|
|
if (input.name) {
|
|
updates.push(`name = $${paramIndex++}`)
|
|
params.push(input.name)
|
|
}
|
|
|
|
if (input.description) {
|
|
updates.push(`description = $${paramIndex++}`)
|
|
params.push(input.description)
|
|
}
|
|
|
|
if (input.status) {
|
|
updates.push(`status = $${paramIndex++}`)
|
|
params.push(input.status)
|
|
}
|
|
|
|
if (input.pricing) {
|
|
updates.push(`pricing_model = $${paramIndex++}`)
|
|
params.push(input.pricing.model)
|
|
|
|
if (input.pricing.basePrice !== undefined) {
|
|
updates.push(`base_price = $${paramIndex++}`)
|
|
params.push(input.pricing.basePrice)
|
|
}
|
|
|
|
if (input.pricing.perRequestPrice !== undefined) {
|
|
updates.push(`per_request_price = $${paramIndex++}`)
|
|
params.push(input.pricing.perRequestPrice)
|
|
}
|
|
|
|
if (input.pricing.freeTier) {
|
|
updates.push(`free_tier = $${paramIndex++}`)
|
|
params.push(JSON.stringify(input.pricing.freeTier))
|
|
}
|
|
}
|
|
|
|
updates.push(`updated_at = NOW()`)
|
|
params.push(id)
|
|
|
|
const result = await db.query(
|
|
`UPDATE api_marketplace_listings SET ${updates.join(', ')} WHERE id = $${paramIndex} RETURNING *`,
|
|
params
|
|
)
|
|
|
|
if (result.rows.length === 0) {
|
|
throw new GraphQLError('Listing not found', {
|
|
extensions: { code: 'NOT_FOUND' },
|
|
})
|
|
}
|
|
|
|
const row = result.rows[0]
|
|
const ratingResult = await db.query(
|
|
'SELECT AVG(rating) as rating, COUNT(*) as count FROM api_marketplace_reviews WHERE listing_id = $1',
|
|
[id]
|
|
)
|
|
|
|
return {
|
|
id: row.id,
|
|
name: row.name,
|
|
description: row.description,
|
|
provider: row.provider,
|
|
endpoint: row.endpoint,
|
|
documentationUrl: row.documentation_url,
|
|
category: row.category,
|
|
pricing: {
|
|
model: row.pricing_model,
|
|
basePrice: row.base_price,
|
|
perRequestPrice: row.per_request_price,
|
|
freeTier: row.free_tier ? JSON.parse(row.free_tier) : null,
|
|
},
|
|
rating: parseFloat(ratingResult.rows[0]?.rating) || 0,
|
|
reviewCount: parseInt(ratingResult.rows[0]?.count) || 0,
|
|
requestCount: 0,
|
|
status: row.status,
|
|
createdAt: new Date(row.created_at),
|
|
updatedAt: new Date(row.updated_at),
|
|
}
|
|
}
|
|
|
|
export async function deleteAPIMarketplaceListing(context: Context, id: string): Promise<boolean> {
|
|
if (!context.user) {
|
|
throw new GraphQLError('Authentication required', {
|
|
extensions: { code: 'UNAUTHENTICATED' },
|
|
})
|
|
}
|
|
|
|
const db = getDb()
|
|
await db.query('DELETE FROM api_subscriptions WHERE listing_id = $1', [id])
|
|
await db.query('DELETE FROM api_marketplace_reviews WHERE listing_id = $1', [id])
|
|
await db.query('DELETE FROM api_marketplace_listings WHERE id = $1', [id])
|
|
|
|
return true
|
|
}
|
|
|
|
export async function subscribeToAPI(context: Context, listingId: string): Promise<APISubscription> {
|
|
if (!context.user) {
|
|
throw new GraphQLError('Authentication required', {
|
|
extensions: { code: 'UNAUTHENTICATED' },
|
|
})
|
|
}
|
|
|
|
const db = getDb()
|
|
|
|
// Check if already subscribed
|
|
const existing = await db.query(
|
|
'SELECT * FROM api_subscriptions WHERE user_id = $1 AND listing_id = $2',
|
|
[context.user.id, listingId]
|
|
)
|
|
|
|
if (existing.rows.length > 0) {
|
|
throw new GraphQLError('Already subscribed', {
|
|
extensions: { code: 'VALIDATION_ERROR' },
|
|
})
|
|
}
|
|
|
|
// Generate API key
|
|
const apiKey = `sk_${crypto.randomBytes(32).toString('hex')}`
|
|
|
|
const result = await db.query(
|
|
`INSERT INTO api_subscriptions (listing_id, user_id, status, api_key, created_at, updated_at)
|
|
VALUES ($1, $2, 'ACTIVE', $3, NOW(), NOW())
|
|
RETURNING *`,
|
|
[listingId, context.user.id, apiKey]
|
|
)
|
|
|
|
const row = result.rows[0]
|
|
return {
|
|
id: row.id,
|
|
listingId: row.listing_id,
|
|
userId: row.user_id,
|
|
status: row.status,
|
|
apiKey: row.api_key,
|
|
createdAt: new Date(row.created_at),
|
|
updatedAt: new Date(row.updated_at),
|
|
}
|
|
}
|
|
|
|
export async function unsubscribeFromAPI(context: Context, subscriptionId: string): Promise<boolean> {
|
|
if (!context.user) {
|
|
throw new GraphQLError('Authentication required', {
|
|
extensions: { code: 'UNAUTHENTICATED' },
|
|
})
|
|
}
|
|
|
|
const db = getDb()
|
|
|
|
// Check ownership
|
|
const sub = await db.query(
|
|
'SELECT user_id FROM api_subscriptions WHERE id = $1',
|
|
[subscriptionId]
|
|
)
|
|
|
|
if (sub.rows.length === 0) {
|
|
throw new GraphQLError('Subscription not found', {
|
|
extensions: { code: 'NOT_FOUND' },
|
|
})
|
|
}
|
|
|
|
if (sub.rows[0].user_id !== context.user.id) {
|
|
throw new GraphQLError('Permission denied', {
|
|
extensions: { code: 'FORBIDDEN' },
|
|
})
|
|
}
|
|
|
|
await db.query(
|
|
'UPDATE api_subscriptions SET status = $1, updated_at = NOW() WHERE id = $2',
|
|
['CANCELLED', subscriptionId]
|
|
)
|
|
|
|
return true
|
|
}
|
|
|