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

420 lines 19.3 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __exportStar = (this && this.__exportStar) || function(m, exports) { for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p); }; Object.defineProperty(exports, "__esModule", { value: true }); exports.generateTypes = exports.TypeGenerator = exports.ModelDelegate = exports.GeneratedClient = exports.Stitcher = exports.MSSQLNativeAdapter = exports.MSSQLAdapter = exports.HttpApiAdapter = exports.PostgresAdapter = exports.DSLParser = exports.ORMClient = void 0; const stitcher_1 = require("./runtime/stitcher"); const parser_1 = require("./parser"); const postgres_1 = require("./adapters/postgres"); const http_1 = require("./adapters/http"); const mssql_1 = require("./adapters/mssql"); const mongodb_1 = require("./adapters/mongodb"); const mysql_1 = require("./adapters/mysql"); const generated_client_1 = require("./client/generated-client"); const logger_1 = require("./utils/logger"); const middleware_1 = require("./types/middleware"); /** * ORM Client Factory - Used to generate type-safe clients from schema * * Usage: * ```typescript * import { ORMClient } from '@your-org/orm'; * import * as fs from 'fs'; * * const schema = fs.readFileSync('./schema.prisma', 'utf-8'); * const orm = new ORMClient(schema); * const client = orm.generate(); * * // Now use type-safe methods * const users = await client.User.findMany({ where: { active: true } }); * ``` */ class ORMClient { ir; stitcher; adapters; schemaPath; middlewareChain; queryContext; constructor(schema, options) { const parser = new parser_1.DSLParser(); this.ir = parser.parse(schema); this.adapters = this.initializeAdapters(); this.schemaPath = options?.schemaPath; this.stitcher = new stitcher_1.Stitcher(this.ir, this.adapters, this.schemaPath, options?.cache); this.middlewareChain = new middleware_1.MiddlewareChain(); this.queryContext = {}; // Empty context by default } /** * 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 middleware chain (for GeneratedClient to use) */ getMiddlewareChain() { return this.middlewareChain; } /** * Generate a type-safe client with model delegates * This is similar to Prisma's generated client pattern * * Checks if TypeScript types have been generated and suggests running * the generator if they don't exist or are out of date. */ generate() { // Check for generated types const typesChecked = this.checkGeneratedTypes(); if (!typesChecked.exists) { console.warn('\n⚠️ Generated TypeScript types not found!'); console.warn(' For full IntelliSense support, run:'); if (this.schemaPath) { console.warn(` npx orm generate ${this.schemaPath}`); } else { console.warn(' npx orm generate ./schema.prisma'); } console.warn(' Or: npm run generate\n'); } else if (typesChecked.outOfDate) { console.warn('\n⚠️ Schema may have changed since types were generated.'); console.warn(' Consider regenerating types:'); if (this.schemaPath) { console.warn(` npx orm generate ${this.schemaPath}`); } else { console.warn(' npx orm generate ./schema.prisma'); } console.warn(' Or: npm run generate\n'); } else { console.log('✅ Using generated types from:', typesChecked.path); } return new generated_client_1.GeneratedClient(this.ir, this.stitcher, this.adapters, this.middlewareChain, this.queryContext); } /** * Check if generated types exist and if they're up to date */ checkGeneratedTypes() { const fs = require('fs'); const path = require('path'); // Common paths to check for generated types const possiblePaths = [ './.qts/types.d.ts', // For .qts files './generated/types.d.ts', './Example/generated/types.d.ts', './Example/.qts/types.d.ts', // For .qts files in Example './src/generated/types.d.ts', path.join(process.cwd(), '.qts', 'types.d.ts'), path.join(process.cwd(), 'generated', 'types.d.ts'), path.join(process.cwd(), 'Example', 'generated', 'types.d.ts'), path.join(process.cwd(), 'Example', '.qts', 'types.d.ts'), ]; for (const typesPath of possiblePaths) { try { if (fs.existsSync(typesPath)) { // Check if types file mentions all our models const typesContent = fs.readFileSync(typesPath, 'utf-8'); const modelNames = Object.keys(this.ir.models); const allModelsPresent = modelNames.every(name => typesContent.includes(`export type ${name} =`) || typesContent.includes(`export interface ${name}Delegate`)); if (!allModelsPresent) { return { exists: true, outOfDate: true, path: typesPath }; } // Check timestamp if schema path is available if (this.schemaPath && fs.existsSync(this.schemaPath)) { const schemaStats = fs.statSync(this.schemaPath); const typesStats = fs.statSync(typesPath); if (schemaStats.mtime > typesStats.mtime) { return { exists: true, outOfDate: true, path: typesPath }; } } return { exists: true, outOfDate: false, path: typesPath }; } } catch (error) { // Continue to next path } } return { exists: false, outOfDate: false }; } initializeAdapters() { const adapters = new Map(); logger_1.logger.debug('runtime', 'Initializing adapters for datasources:', Object.keys(this.ir.datasources)); for (const [name, datasource] of Object.entries(this.ir.datasources)) { logger_1.logger.debug('runtime', `Creating adapter for datasource '${name}' with provider '${datasource.provider}'`); if (datasource.provider === 'postgres' || datasource.provider === 'postgresql') { const url = this.resolveEnvVar(datasource.url); const adapter = new postgres_1.PostgresAdapter(url); // Set model metadata for @map support if (adapter.setModelMetadata) { adapter.setModelMetadata(this.ir.models); } adapters.set(name, adapter); } else if (datasource.provider === 'mongodb' || datasource.provider === 'mongo') { const url = this.resolveEnvVar(datasource.url); const adapter = new mongodb_1.MongoDBAdapter(url); // Set model metadata if (adapter.setModelMetadata) { adapter.setModelMetadata(this.ir.models); } adapters.set(name, adapter); } else if (datasource.provider === 'mysql') { const url = this.resolveEnvVar(datasource.url); const adapter = new mysql_1.MySQLAdapter(url); // Set model metadata for @map support if (adapter.setModelMetadata) { adapter.setModelMetadata(this.ir.models); } adapters.set(name, adapter); } else if (datasource.provider === 'mssql' || datasource.provider === 'sqlserver') { const url = this.resolveEnvVar(datasource.url); const config = this.parseMSSQLConnectionString(url); logger_1.logger.debug('runtime', 'MSSQL config:', JSON.stringify(config, null, 2)); // Use ODBC adapter (MSSQLAdapter) for all SQL Server connections logger_1.logger.debug('runtime', 'Using MSSQLAdapter (ODBC) for SQL Server'); const adapter = new mssql_1.MSSQLAdapter(config); // Set model metadata for @map support if (adapter.setModelMetadata) { adapter.setModelMetadata(this.ir.models); } adapters.set(name, adapter); } else if (datasource.provider === 'http') { const baseUrl = datasource.baseUrl; // For HTTP adapters, collect all models for this datasource const modelsForDatasource = Object.values(this.ir.models).filter(m => m.datasource === name); if (modelsForDatasource.length > 0) { logger_1.logger.debug('runtime', `Creating HTTP adapter for datasource '${name}' with ${modelsForDatasource.length} model(s)`); for (const model of modelsForDatasource) { logger_1.logger.debug('runtime', `HTTP Model '${model.name}' endpoints:`, JSON.stringify(model.endpoints, null, 2)); } // Create one adapter with all models for this datasource // TODO: Load OAuth hook if configured const adapter = new http_1.HttpApiAdapter(baseUrl, modelsForDatasource); adapters.set(name, adapter); } } } return adapters; } parseMSSQLConnectionString(connectionString) { // Parse connection string like: Server=localhost;Database=mydb;User Id=sa;Password=pass; // Or for Windows Auth: Server=localhost;Database=mydb;Integrated Security=true;Domain=DOMAIN logger_1.logger.debug('runtime', 'Parsing connection string:', connectionString); const parts = connectionString.split(';').reduce((acc, part) => { const [key, value] = part.split('='); if (key && value) { const k = key.trim().toLowerCase(); const v = value.trim(); logger_1.logger.debug('runtime', `Connection string part: ${k} = ${v}`); acc[k] = v; } return acc; }, {}); logger_1.logger.debug('runtime', 'Parsed connection string parts:', JSON.stringify(parts, null, 2)); const useWindowsAuth = parts['integrated security'] === 'true' || parts['integrated security'] === 'sspi' || parts['trusted_connection'] === 'true' || parts['trusted_connection'] === 'yes'; logger_1.logger.debug('runtime', 'Use Windows Auth?', useWindowsAuth); const config = { server: parts['server'] || parts['data source'] || 'localhost', database: parts['database'] || parts['initial catalog'] || 'master', options: { trustedConnection: useWindowsAuth, trustServerCertificate: parts['trustservercertificate'] === 'true' || true, encrypt: parts['encrypt'] === 'true' || parts['encrypt'] === '1', port: parseInt(parts['port'] || '1433') } }; if (useWindowsAuth) { // Windows Authentication - config for native adapter config.domain = parts['domain'] || ''; // Also keep tedious-style config for backward compatibility config.authentication = { type: 'ntlm', options: { domain: config.domain, userName: process.env.USERNAME || '', password: '' } }; } else { // SQL Server Authentication config.authentication = { type: 'default', options: { userName: parts['user id'] || parts['uid'] || 'sa', password: parts['password'] || parts['pwd'] || '' } }; } return config; } resolveEnvVar(value) { const match = value.match(/env\(["']?(.+?)["']?\)/); if (match) { const envVar = process.env[match[1]]; if (!envVar) { throw new Error(`Environment variable ${match[1]} is not set`); } return envVar; } return value; } /** * @deprecated Use generate() to create a type-safe client instead * @example * const client = orm.generate(); * await client.User.findMany({ ... }); */ async findMany(model, args = {}) { console.warn('[DEPRECATED] Direct orm.findMany() is deprecated. Use orm.generate() to create a type-safe client.'); return this.stitcher.findMany(model, args); } /** * @deprecated Use generate() to create a type-safe client instead */ async findUnique(model, args) { console.warn('[DEPRECATED] Direct orm.findUnique() is deprecated. Use orm.generate() to create a type-safe client.'); return this.stitcher.findUnique(model, args); } /** * @deprecated Use generate() to create a type-safe client instead */ async count(model, args = {}) { console.warn('[DEPRECATED] Direct orm.count() is deprecated. Use orm.generate() to create a type-safe client.'); const modelDef = this.ir.models[model]; if (!modelDef) throw new Error(`Model ${model} not found`); const adapter = this.adapters.get(modelDef.datasource); if (!adapter) throw new Error(`No adapter for datasource ${modelDef.datasource}`); return adapter.count(model, { where: args.where }); } /** * @deprecated Use generate() to create a type-safe client instead */ async create(model, args) { console.warn('[DEPRECATED] Direct orm.create() is deprecated. Use orm.generate() to create a type-safe client.'); const modelDef = this.ir.models[model]; if (!modelDef) throw new Error(`Model ${model} not found`); const adapter = this.adapters.get(modelDef.datasource); if (!adapter) throw new Error(`No adapter for datasource ${modelDef.datasource}`); return adapter.create(model, { data: args.data }); } /** * @deprecated Use generate() to create a type-safe client instead */ async update(model, args) { console.warn('[DEPRECATED] Direct orm.update() is deprecated. Use orm.generate() to create a type-safe client.'); const modelDef = this.ir.models[model]; if (!modelDef) throw new Error(`Model ${model} not found`); const adapter = this.adapters.get(modelDef.datasource); if (!adapter) throw new Error(`No adapter for datasource ${modelDef.datasource}`); return adapter.update(model, { where: args.where, data: args.data }); } /** * @deprecated Use generate() to create a type-safe client instead */ async delete(model, args) { console.warn('[DEPRECATED] Direct orm.delete() is deprecated. Use orm.generate() to create a type-safe client.'); const modelDef = this.ir.models[model]; if (!modelDef) throw new Error(`Model ${model} not found`); const adapter = this.adapters.get(modelDef.datasource); if (!adapter) throw new Error(`No adapter for datasource ${modelDef.datasource}`); return adapter.delete(model, { where: args.where }); } /** * @deprecated Use client.$disconnect() instead */ async close() { console.warn('[DEPRECATED] orm.close() is deprecated. Use client.$disconnect() instead.'); for (const adapter of this.adapters.values()) { if ('close' in adapter && typeof adapter.close === 'function') { await adapter.close(); } } } } exports.ORMClient = ORMClient; // Export types and utilities __exportStar(require("./types/ir"), exports); __exportStar(require("./types/adapter"), exports); var parser_2 = require("./parser"); Object.defineProperty(exports, "DSLParser", { enumerable: true, get: function () { return parser_2.DSLParser; } }); var postgres_2 = require("./adapters/postgres"); Object.defineProperty(exports, "PostgresAdapter", { enumerable: true, get: function () { return postgres_2.PostgresAdapter; } }); var http_2 = require("./adapters/http"); Object.defineProperty(exports, "HttpApiAdapter", { enumerable: true, get: function () { return http_2.HttpApiAdapter; } }); var mssql_2 = require("./adapters/mssql"); Object.defineProperty(exports, "MSSQLAdapter", { enumerable: true, get: function () { return mssql_2.MSSQLAdapter; } }); var mssql_3 = require("./adapters/mssql"); Object.defineProperty(exports, "MSSQLNativeAdapter", { enumerable: true, get: function () { return mssql_3.MSSQLNativeAdapter; } }); var stitcher_2 = require("./runtime/stitcher"); Object.defineProperty(exports, "Stitcher", { enumerable: true, get: function () { return stitcher_2.Stitcher; } }); var generated_client_2 = require("./client/generated-client"); Object.defineProperty(exports, "GeneratedClient", { enumerable: true, get: function () { return generated_client_2.GeneratedClient; } }); Object.defineProperty(exports, "ModelDelegate", { enumerable: true, get: function () { return generated_client_2.ModelDelegate; } }); var typegen_1 = require("./generator/typegen"); Object.defineProperty(exports, "TypeGenerator", { enumerable: true, get: function () { return typegen_1.TypeGenerator; } }); Object.defineProperty(exports, "generateTypes", { enumerable: true, get: function () { return typegen_1.generateTypes; } }); //# sourceMappingURL=index.js.map