@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
JavaScript
"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