@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
365 lines • 13.4 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.GeneratedClient = exports.ModelDelegate = void 0;
/**
* Model delegate provides type-safe methods for a specific model
*/
class ModelDelegate {
modelName;
stitcher;
adapter;
ir;
middlewareChain;
getContext;
constructor(modelName, stitcher, adapter, ir, middlewareChain, getContext) {
this.modelName = modelName;
this.stitcher = stitcher;
this.adapter = adapter;
this.ir = ir;
this.middlewareChain = middlewareChain;
this.getContext = getContext;
}
/**
* Execute query with middleware hooks
*/
async executeWithMiddleware(operation, args, executor) {
const context = this.getContext();
try {
// Execute beforeQuery hooks
const modifiedArgs = await this.middlewareChain.executeBeforeQuery(this.modelName, operation, args || {}, context);
// Execute the actual query
const result = await executor(modifiedArgs);
// Execute afterQuery hooks
const modifiedResult = await this.middlewareChain.executeAfterQuery(this.modelName, operation, modifiedArgs, result, context);
return modifiedResult;
}
catch (error) {
// Execute error hooks
await this.middlewareChain.executeOnError(this.modelName, operation, args || {}, error, context);
// Re-throw the error
throw error;
}
}
async findMany(args) {
return this.executeWithMiddleware('findMany', args, async (modifiedArgs) => {
const result = await this.stitcher.findMany(this.modelName, modifiedArgs || {});
return result.data;
});
}
async findUnique(args) {
return this.executeWithMiddleware('findUnique', args, async (modifiedArgs) => {
const result = await this.stitcher.findUnique(this.modelName, modifiedArgs);
return result.data;
});
}
async findFirst(args) {
return this.executeWithMiddleware('findMany', args, async (modifiedArgs) => {
const result = await this.stitcher.findMany(this.modelName, {
...modifiedArgs,
take: 1
});
return result.data[0] || null;
});
}
async count(args) {
return this.executeWithMiddleware('count', args, async (modifiedArgs) => {
return this.adapter.count(this.modelName, { where: modifiedArgs?.where });
});
}
async create(args) {
return this.executeWithMiddleware('create', args, async (modifiedArgs) => {
const result = await this.adapter.create(this.modelName, { data: modifiedArgs.data });
// Invalidate cache for this model
this.stitcher.invalidateCache(this.modelName);
return result;
});
}
async createMany(args) {
return this.executeWithMiddleware('createMany', args, async (modifiedArgs) => {
let count = 0;
for (const item of modifiedArgs.data) {
try {
await this.adapter.create(this.modelName, { data: item });
count++;
}
catch (error) {
if (!modifiedArgs.skipDuplicates)
throw error;
}
}
// Invalidate cache for this model
this.stitcher.invalidateCache(this.modelName);
return { count };
});
}
async update(args) {
return this.executeWithMiddleware('update', args, async (modifiedArgs) => {
const result = await this.adapter.update(this.modelName, { where: modifiedArgs.where, data: modifiedArgs.data });
// Invalidate cache for this model
this.stitcher.invalidateCache(this.modelName);
return result;
});
}
async updateMany(args) {
return this.executeWithMiddleware('updateMany', args, async (modifiedArgs) => {
// For now, simplified implementation
// TODO: Add batch update support to adapters
await this.adapter.update(this.modelName, { where: modifiedArgs.where || {}, data: modifiedArgs.data });
// Invalidate cache for this model
this.stitcher.invalidateCache(this.modelName);
return { count: 1 }; // Placeholder
});
}
async delete(args) {
return this.executeWithMiddleware('delete', args, async (modifiedArgs) => {
const result = await this.adapter.delete(this.modelName, { where: modifiedArgs.where });
// Invalidate cache for this model
this.stitcher.invalidateCache(this.modelName);
return result;
});
}
async deleteMany(args) {
return this.executeWithMiddleware('deleteMany', args, async (modifiedArgs) => {
// For now, simplified implementation
// TODO: Add batch delete support to adapters
await this.adapter.delete(this.modelName, { where: modifiedArgs?.where || {} });
// Invalidate cache for this model
this.stitcher.invalidateCache(this.modelName);
return { count: 1 }; // Placeholder
});
}
async upsert(args) {
return this.executeWithMiddleware('upsert', args, async (modifiedArgs) => {
// Try to find existing record
const existing = await this.findUnique({ where: modifiedArgs.where });
if (existing) {
// Update existing
return this.adapter.update(this.modelName, {
where: modifiedArgs.where,
data: modifiedArgs.update
});
}
else {
// Create new
return this.adapter.create(this.modelName, {
data: modifiedArgs.create
});
}
});
}
}
exports.ModelDelegate = ModelDelegate;
/**
* Generated client with model delegates
*/
class GeneratedClient {
ir;
stitcher;
adapters;
middlewareChain;
queryContext;
delegates = new Map();
datasourceQueryDelegates = new Map();
constructor(ir, stitcher, adapters, middlewareChain, queryContext) {
this.ir = ir;
this.stitcher = stitcher;
this.adapters = adapters;
this.middlewareChain = middlewareChain;
this.queryContext = queryContext;
this.generateDelegates();
this.generateDatasourceQueryDelegates();
}
generateDelegates() {
for (const [modelName, model] of Object.entries(this.ir.models)) {
const adapter = this.adapters.get(model.datasource);
if (!adapter) {
throw new Error(`No adapter found for datasource: ${model.datasource}`);
}
const delegate = new ModelDelegate(modelName, this.stitcher, adapter, this.ir, this.middlewareChain, () => this.queryContext);
this.delegates.set(modelName, delegate);
// Create property accessor for model delegate
Object.defineProperty(this, modelName, {
get: () => delegate,
enumerable: true,
configurable: false
});
}
}
/**
* Generate datasource query delegates for raw SQL queries
* Creates properties like client.main_db.query(sql)
*/
generateDatasourceQueryDelegates() {
for (const [datasourceName, adapter] of this.adapters.entries()) {
// Only create query delegate if adapter supports raw queries
if ('query' in adapter && typeof adapter.query === 'function') {
const queryDelegate = {
query: async (sql, params) => {
return adapter.query(sql, params);
}
};
this.datasourceQueryDelegates.set(datasourceName, queryDelegate);
// Create property accessor for datasource query delegate
Object.defineProperty(this, datasourceName, {
get: () => queryDelegate,
enumerable: true,
configurable: false
});
}
}
}
/**
* Register middleware for query interception
*
* Example:
* ```typescript
* client.use({
* name: 'tenant-isolation',
* beforeQuery: ({ model, args, context }) => {
* if (context.user?.tenantId) {
* args.where = { ...args.where, tenant_id: context.user.tenantId };
* }
* return args;
* }
* });
* ```
*/
use(middleware) {
this.middlewareChain.use(middleware);
}
/**
* Set query context (user, tenantId, etc.)
* This context is available to all middleware hooks
*
* Example:
* ```typescript
* client.setContext({
* user: { id: 123, tenantId: 456, role: 'admin' }
* });
* ```
*/
setContext(context) {
this.queryContext = context;
}
/**
* Get current query context
*/
getContext() {
return this.queryContext;
}
/**
* Get a model delegate by name
*/
model(modelName) {
const delegate = this.delegates.get(modelName);
if (!delegate) {
throw new Error(`Model '${modelName}' not found in schema`);
}
return delegate;
}
/**
* Close all adapter connections
*/
async $disconnect() {
for (const adapter of this.adapters.values()) {
if ('close' in adapter && typeof adapter.close === 'function') {
await adapter.close();
}
}
}
/**
* Raw query execution (datasource-specific)
*/
async $queryRaw(datasourceName, query, params) {
const adapter = this.adapters.get(datasourceName);
if (!adapter) {
throw new Error(`Datasource '${datasourceName}' not found`);
}
// Check if adapter supports raw queries
if ('query' in adapter && typeof adapter.query === 'function') {
return adapter.query(query, params);
}
throw new Error(`Adapter for '${datasourceName}' does not support raw queries`);
}
/**
* Transaction support - Execute multiple operations in a transaction
*
* Example:
* ```typescript
* await client.$transaction(async (tx) => {
* const user = await tx.User.create({ data: { email: 'test@example.com' } });
* const order = await tx.Orders.create({ data: { userId: user.id } });
* return { user, order };
* });
* ```
*
* With options:
* ```typescript
* await client.$transaction(async (tx) => {
* // ...operations
* }, {
* isolationLevel: 'SERIALIZABLE',
* timeout: 10000
* });
* ```
*/
async $transaction(callback, options) {
// Find a SQL adapter that supports transactions
let transactionAdapter = null;
let datasourceName = null;
for (const [name, adapter] of this.adapters.entries()) {
if (adapter.capabilities.transactions && 'beginTransaction' in adapter) {
transactionAdapter = adapter;
datasourceName = name;
break;
}
}
if (!transactionAdapter || !datasourceName) {
throw new Error('No adapter with transaction support found. Transactions require PostgreSQL or MSSQL adapters.');
}
// Begin transaction
const tx = await transactionAdapter.beginTransaction(options);
// Create a transaction-scoped client
const txAdapters = new Map(this.adapters);
txAdapters.set(datasourceName, tx); // Replace adapter with transaction
const txClient = new GeneratedClient(this.ir, this.stitcher, txAdapters, this.middlewareChain, this.queryContext);
try {
const result = await callback(txClient);
await tx.commit();
return result;
}
catch (error) {
await tx.rollback();
throw error;
}
}
/**
* Execute a raw SQL query on the first available SQL datasource
*
* Example:
* ```typescript
* const result = await client.generalQuery('SELECT * FROM users WHERE id = $1', [123]);
* console.log(result);
* ```
*
* Note: For MSSQL, use @p1, @p2, etc. instead of $1, $2:
* ```typescript
* const result = await client.generalQuery('SELECT * FROM users WHERE id = @p1', [123]);
* ```
*
* @param sql - The SQL query string
* @param params - Optional query parameters
* @returns Query result rows
*/
async generalQuery(sql, params) {
// Find the first SQL adapter that supports raw queries
for (const [datasourceName, adapter] of this.adapters.entries()) {
if ('query' in adapter && typeof adapter.query === 'function') {
return adapter.query(sql, params);
}
}
throw new Error('No SQL datasource with query support found. Raw queries require PostgreSQL or MSSQL adapters.');
}
}
exports.GeneratedClient = GeneratedClient;
//# sourceMappingURL=generated-client.js.map