UNPKG

@aradox/multi-orm

Version:

Type-safe ORM with multi-datasource support, row-level security, and Prisma-like API for PostgreSQL, SQL Server, and HTTP APIs

289 lines 13.1 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.HttpApiAdapter = void 0; const axios_1 = __importDefault(require("axios")); const logger_1 = require("../utils/logger"); class HttpApiAdapter { client; models = new Map(); oauthHook; cachedOAuth; storage = new Map(); capabilities; constructor(baseUrl, models, oauthHook) { this.client = axios_1.default.create({ baseURL: baseUrl, timeout: 10000 }); // Support both single model and array of models const modelArray = Array.isArray(models) ? models : [models]; for (const model of modelArray) { this.models.set(model.name, model); } this.oauthHook = oauthHook; // Use capabilities from first model (or merge them) const firstModel = modelArray[0]; this.capabilities = { transactions: false, bulkByIds: !!firstModel.endpoints?.findManyBulkById, maxIn: 1000, supportedOperators: ['eq', 'in'] }; } getModel(modelName) { const model = this.models.get(modelName); if (!model) { throw new Error(`Model '${modelName}' not found in HTTP adapter. Available models: ${Array.from(this.models.keys()).join(', ')}`); } return model; } async findMany(modelName, args) { logger_1.logger.debug('http', 'findMany called for model:', modelName); logger_1.logger.debug('http', 'findMany args:', JSON.stringify(args, null, 2)); const model = this.getModel(modelName); logger_1.logger.debug('http', 'Model endpoints:', JSON.stringify(model.endpoints, null, 2)); const endpoint = model.endpoints?.findMany; if (!endpoint) { logger_1.logger.error('http', 'No findMany endpoint configured for model:', modelName); throw new Error(`No findMany endpoint for model ${modelName}`); } // Check if we should use bulk endpoint if (args.where?.id?.in && model.endpoints?.findManyBulkById) { return this.findManyBulkById(modelName, args.where.id.in); } const { url, config } = await this.buildRequest(endpoint, { args }); // Add WHERE clause as query params for simple filters if (args.where && !config.params) { config.params = {}; } if (args.where) { for (const [key, value] of Object.entries(args.where)) { if (value && typeof value === 'object' && 'in' in value) { // For "in" operator, use the first value (or fetch all and filter later) config.params[key] = value.in[0]; logger_1.logger.debug('http', 'Added query param:', key, '=', value.in[0]); } else if (value && typeof value === 'object' && 'eq' in value) { config.params[key] = value.eq; } else { config.params[key] = value; } } } logger_1.logger.debug('http', '🌐 HTTP Request:', config.method || 'GET', url); logger_1.logger.debug('http', '🌐 Query params:', JSON.stringify(config.params)); const response = await this.executeWithRetry(() => this.client.request({ ...config, url })); logger_1.logger.debug('http', 'Response status:', response.status); logger_1.logger.debug('http', 'Response data length:', Array.isArray(response.data) ? response.data.length : 'not array'); const selector = endpoint.response.list || endpoint.response.items || '$'; logger_1.logger.debug('http', 'Using selector:', selector); let result = this.extractFromResponse(response.data, selector); logger_1.logger.debug('http', 'Extracted result length:', Array.isArray(result) ? result.length : 'not array'); // Post-filter for "in" operator with multiple values if (args.where) { for (const [key, value] of Object.entries(args.where)) { if (value && typeof value === 'object' && 'in' in value && value.in.length > 1) { result = result.filter((item) => value.in.includes(item[key])); logger_1.logger.debug('http', 'Post-filtered by', key, 'in', value.in, '- result length:', result.length); } } } return result; } async findManyBulkById(modelName, ids) { const model = this.getModel(modelName); const endpoint = model.endpoints?.findManyBulkById; if (!endpoint) throw new Error('No bulk endpoint available'); const { url, config } = await this.buildRequest(endpoint, { ids }); const response = await this.executeWithRetry(() => this.client.request({ ...config, url })); return this.extractFromResponse(response.data, endpoint.response.items || '$.data'); } async findUnique(modelName, args) { logger_1.logger.debug('http', 'findUnique called for model:', modelName); logger_1.logger.debug('http', 'findUnique args:', JSON.stringify(args, null, 2)); const model = this.getModel(modelName); const endpoint = model.endpoints?.findUnique; if (!endpoint) throw new Error(`No findUnique endpoint for model ${modelName}`); const { url, config } = await this.buildRequest(endpoint, { args }); logger_1.logger.debug('http', 'findUnique URL:', url); logger_1.logger.debug('http', 'findUnique config:', JSON.stringify(config, null, 2)); try { const response = await this.executeWithRetry(() => this.client.request({ ...config, url })); logger_1.logger.debug('http', 'findUnique response status:', response.status); const result = this.extractFromResponse(response.data, endpoint.response.item || '$'); logger_1.logger.debug('http', 'findUnique result:', result); return result; } catch (error) { logger_1.logger.debug('http', 'findUnique error:', error.message); if (axios_1.default.isAxiosError(error) && error.response?.status === 404) { return null; } throw error; } } async create(modelName, args) { const model = this.getModel(modelName); const endpoint = model.endpoints?.create; if (!endpoint) throw new Error(`No create endpoint for model ${modelName}`); const { url, config } = await this.buildRequest(endpoint, { data: args.data }); const response = await this.executeWithRetry(() => this.client.request({ ...config, url })); return this.extractFromResponse(response.data, endpoint.response.item || '$'); } async update(modelName, args) { const model = this.getModel(modelName); const endpoint = model.endpoints?.update; if (!endpoint) throw new Error(`No update endpoint for model ${modelName}`); const { url, config } = await this.buildRequest(endpoint, { where: args.where, data: args.data }); const response = await this.executeWithRetry(() => this.client.request({ ...config, url })); return this.extractFromResponse(response.data, endpoint.response.item || '$'); } async delete(modelName, args) { const model = this.getModel(modelName); const endpoint = model.endpoints?.delete; if (!endpoint) throw new Error(`No delete endpoint for model ${modelName}`); const { url, config } = await this.buildRequest(endpoint, { where: args.where }); const response = await this.executeWithRetry(() => this.client.request({ ...config, url })); return this.extractFromResponse(response.data, endpoint.response.item || '$'); } async count(modelName, args) { const model = this.getModel(modelName); const endpoint = model.endpoints?.count || model.endpoints?.findMany; if (!endpoint) throw new Error(`No count endpoint for model ${modelName}`); const { url, config } = await this.buildRequest(endpoint, { args }); const response = await this.executeWithRetry(() => this.client.request({ ...config, url })); if (endpoint.response.total) { return this.extractFromResponse(response.data, endpoint.response.total); } const items = this.extractFromResponse(response.data, endpoint.response.items || '$.data'); return Array.isArray(items) ? items.length : 0; } async buildRequest(endpoint, context) { let url = endpoint.path; const config = { method: endpoint.method, headers: {} }; // Apply OAuth headers if (this.oauthHook) { const oauthResult = await this.getOAuthHeaders(); Object.assign(config.headers, oauthResult.headers); } // Replace path parameters const whereClause = context.where || context.args?.where; logger_1.logger.debug('http', 'buildRequest whereClause:', whereClause); logger_1.logger.debug('http', 'buildRequest url before:', url); if (whereClause) { for (const [key, value] of Object.entries(whereClause)) { logger_1.logger.debug('http', 'buildRequest: Replacing {' + key + '} with', value); url = url.replace(`{${key}}`, String(value)); } } logger_1.logger.debug('http', 'buildRequest url after:', url); // Build query parameters if (endpoint.query) { const params = {}; for (const [paramKey, mapping] of Object.entries(endpoint.query)) { const value = this.resolveMapping(mapping, context); if (value !== undefined) { params[paramKey] = value; } } config.params = params; } // Build request body if (endpoint.body) { const body = {}; for (const [bodyKey, mapping] of Object.entries(endpoint.body)) { const value = this.resolveMapping(mapping, context); if (value !== undefined) { body[bodyKey] = value; } } config.data = body; } return { url, config }; } resolveMapping(mapping, context) { if (!mapping.startsWith('$')) return mapping; // Remove optional marker const isOptional = mapping.endsWith('?'); const path = mapping.replace(/^\$/, '').replace(/\?$/, ''); const parts = path.split('.'); let value = context; for (const part of parts) { if (value === undefined || value === null) { return isOptional ? undefined : null; } value = value[part]; } return value; } extractFromResponse(data, selector) { if (selector === '$') return data; // Simple JSONPath-like selector const path = selector.replace(/^\$\./, '').split('.'); let value = data; for (const key of path) { if (value === undefined || value === null) return null; value = value[key]; } return value; } async getOAuthHeaders() { if (!this.oauthHook) return {}; // Check cache if (this.cachedOAuth && Date.now() < this.cachedOAuth.expiresAt) { return this.cachedOAuth.result; } // Get fresh OAuth result const ctx = { persist: async (key, value) => { this.storage.set(key, value); }, get: async (key) => this.storage.get(key), set: async (key, value) => { this.storage.set(key, value); } }; const result = await this.oauthHook(ctx); if (result.expiresAt) { this.cachedOAuth = { result, expiresAt: result.expiresAt }; } return result; } async executeWithRetry(fn, retries = 1) { try { return await fn(); } catch (error) { if (axios_1.default.isAxiosError(error) && error.response?.status === 401 && retries > 0) { // Try to refresh OAuth if (this.cachedOAuth?.result.refresh) { const refreshed = await this.cachedOAuth.result.refresh('401'); this.cachedOAuth = { result: { ...this.cachedOAuth.result, ...refreshed }, expiresAt: refreshed.expiresAt || Date.now() + 3600000 }; return this.executeWithRetry(fn, retries - 1); } } throw error; } } } exports.HttpApiAdapter = HttpApiAdapter; //# sourceMappingURL=http.js.map