UNPKG

prisma-entity-framework

Version:

TypeScript Entity Framework for Prisma ORM with Active Record pattern, fluent query builder, relation graphs, batch operations, advanced CRUD, search filters, and pagination utilities.

1,440 lines (1,427 loc) 106 kB
'use strict'; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __hasOwnProp = Object.prototype.hasOwnProperty; var __esm = (fn, res) => function __init() { return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res; }; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // src/rate-limiter.ts function createRateLimiter(options) { switch (options.algorithm) { case "token-bucket": return new exports.TokenBucketRateLimiter(options); case "sliding-window": return new exports.TokenBucketRateLimiter(options); default: throw new Error(`Unknown algorithm: ${options.algorithm}`); } } exports.RateLimiter = void 0; exports.TokenBucketRateLimiter = void 0; var init_rate_limiter = __esm({ "src/rate-limiter.ts"() { exports.RateLimiter = class { constructor(options) { if (options.maxQueriesPerSecond <= 0) { throw new Error("maxQueriesPerSecond must be positive"); } this.options = options; } }; exports.TokenBucketRateLimiter = class extends exports.RateLimiter { constructor(options) { super(options); this.refillRate = options.maxQueriesPerSecond / 1e3; this.bucketCapacity = options.maxQueriesPerSecond; this.tokens = this.bucketCapacity; this.lastRefill = Date.now(); } /** * Refill tokens based on elapsed time */ refill() { const now = Date.now(); const elapsed = now - this.lastRefill; const tokensToAdd = elapsed * this.refillRate; this.tokens = Math.min(this.bucketCapacity, this.tokens + tokensToAdd); this.lastRefill = now; } /** * Acquire permission to execute a query * Waits if no tokens are available */ async acquire() { this.refill(); if (this.tokens >= 1) { this.tokens -= 1; return; } const tokensNeeded = 1 - this.tokens; const waitTime = Math.ceil(tokensNeeded / this.refillRate); await new Promise((resolve) => setTimeout(resolve, waitTime)); this.refill(); this.tokens -= 1; } /** * Get current status */ getStatus() { this.refill(); return { available: Math.floor(this.tokens), total: this.bucketCapacity, utilization: 1 - this.tokens / this.bucketCapacity }; } /** * Reset the rate limiter */ reset() { this.tokens = this.bucketCapacity; this.lastRefill = Date.now(); } }; } }); // src/config.ts var config_exports = {}; __export(config_exports, { configurePrisma: () => configurePrisma, getConfig: () => getConfig, getConnectionPoolSize: () => getConnectionPoolSize, getMaxConcurrency: () => getMaxConcurrency, getPrismaInstance: () => getPrismaInstance, getRateLimiter: () => getRateLimiter, isParallelEnabled: () => isParallelEnabled, isPrismaConfigured: () => isPrismaConfigured, resetPrismaConfiguration: () => resetPrismaConfiguration }); function configurePrisma(prisma, config) { if (!prisma) { throw new Error("Prisma client instance is required"); } if (config) { if (config.maxConcurrency !== void 0) { if (!Number.isInteger(config.maxConcurrency) || config.maxConcurrency < 1) { throw new Error("maxConcurrency must be a positive integer"); } } if (config.maxQueriesPerSecond !== void 0) { if (!Number.isFinite(config.maxQueriesPerSecond) || config.maxQueriesPerSecond <= 0) { throw new Error("maxQueriesPerSecond must be a positive number"); } } globalConfig = { ...globalConfig, ...config }; } globalPrismaInstance = prisma; globalRateLimiter = createRateLimiter({ maxQueriesPerSecond: globalConfig.maxQueriesPerSecond || 100, algorithm: "token-bucket" }); } function getPrismaInstance() { if (!globalPrismaInstance) { throw new Error( "Prisma instance not configured. Call configurePrisma(prisma) before using any entity operations." ); } return globalPrismaInstance; } function isPrismaConfigured() { return globalPrismaInstance !== null; } function resetPrismaConfiguration() { globalPrismaInstance = null; globalRateLimiter = null; globalConfig = { maxConcurrency: void 0, enableParallel: true, maxQueriesPerSecond: 100 }; } function getDatabaseProvider(prisma) { try { const datasources = prisma?._engineConfig?.datasources; if (datasources && datasources.length > 0) { return datasources[0].activeProvider || "unknown"; } const url = process.env.DATABASE_URL || ""; if (url.startsWith("postgresql://") || url.startsWith("postgres://")) return "postgresql"; if (url.startsWith("mysql://")) return "mysql"; if (url.startsWith("mongodb://") || url.startsWith("mongodb+srv://")) return "mongodb"; if (url.startsWith("sqlserver://")) return "sqlserver"; if (url.includes("file:") || url.includes(".db")) return "sqlite"; return "unknown"; } catch { return "unknown"; } } function getConnectionPoolSize() { try { const prisma = getPrismaInstance(); const datasourceUrl = process.env.DATABASE_URL; if (datasourceUrl) { try { const url = new URL(datasourceUrl); const connectionLimit = url.searchParams.get("connection_limit"); if (connectionLimit) { const limit = parseInt(connectionLimit, 10); if (!isNaN(limit) && limit > 0) { return limit; } } const poolSize = url.searchParams.get("pool_size"); if (poolSize) { const size = parseInt(poolSize, 10); if (!isNaN(size) && size > 0) { return size; } } const maxPoolSize = url.searchParams.get("maxPoolSize"); if (maxPoolSize) { const size = parseInt(maxPoolSize, 10); if (!isNaN(size) && size > 0) { return size; } } } catch { } } const clientConfig = prisma?._engineConfig; if (clientConfig?.datasources?.[0]?.url) { try { const url = new URL(clientConfig.datasources[0].url); const connectionLimit = url.searchParams.get("connection_limit"); if (connectionLimit) { const limit = parseInt(connectionLimit, 10); if (!isNaN(limit) && limit > 0) { return limit; } } } catch { } } const provider = getDatabaseProvider(prisma); const defaultPoolSizes = { postgresql: 10, // PostgreSQL default mysql: 10, // MySQL default sqlite: 1, // SQLite is single-threaded sqlserver: 10, // SQL Server default mongodb: 10 // MongoDB default }; return defaultPoolSizes[provider] || 2; } catch (error) { return 2; } } function getMaxConcurrency() { if (globalConfig.maxConcurrency !== void 0) { return globalConfig.maxConcurrency; } return getConnectionPoolSize(); } function isParallelEnabled() { if (globalConfig.enableParallel === false) { return false; } const poolSize = getConnectionPoolSize(); return poolSize > 1; } function getConfig() { return { ...globalConfig }; } function getRateLimiter() { return globalRateLimiter; } var globalPrismaInstance, globalConfig, globalRateLimiter; var init_config = __esm({ "src/config.ts"() { init_rate_limiter(); globalPrismaInstance = null; globalConfig = { maxConcurrency: void 0, enableParallel: true, maxQueriesPerSecond: 100 }; globalRateLimiter = null; } }); // src/index.ts init_config(); init_rate_limiter(); // src/parallel-utils.ts init_config(); var ParallelMetricsTracker = class { constructor(totalOperations, concurrency) { this.startTime = 0; this.endTime = 0; this.operationTimes = []; this.totalOperations = 0; this.concurrency = 1; this.totalOperations = totalOperations; this.concurrency = concurrency; } /** * Start tracking */ start() { this.startTime = Date.now(); } /** * Record an operation completion */ recordOperation(duration) { this.operationTimes.push(duration); } /** * Complete tracking and calculate metrics */ complete() { this.endTime = Date.now(); const totalTime = this.endTime - this.startTime; const avgOperationTime = this.operationTimes.length > 0 ? this.operationTimes.reduce((sum, t) => sum + t, 0) / this.operationTimes.length : 0; const sequentialEstimate = avgOperationTime * this.totalOperations; const speedupFactor = sequentialEstimate > 0 ? sequentialEstimate / totalTime : 1; const itemsPerSecond = totalTime > 0 ? this.totalOperations / totalTime * 1e3 : 0; const idealSpeedup = Math.min(this.concurrency, this.totalOperations); const parallelEfficiency = idealSpeedup > 0 ? speedupFactor / idealSpeedup : 0; const connectionUtilization = this.totalOperations >= this.concurrency ? Math.min(1, speedupFactor / this.concurrency) : this.totalOperations / this.concurrency; return { totalTime, sequentialEstimate, speedupFactor, itemsPerSecond, parallelEfficiency: Math.min(1, parallelEfficiency), connectionUtilization: Math.min(1, connectionUtilization) }; } }; function createParallelMetrics() { return { totalTime: 0, sequentialEstimate: 0, speedupFactor: 1, itemsPerSecond: 0, parallelEfficiency: 0, connectionUtilization: 0 }; } async function executeInParallel(operations, options) { if (operations.length === 0) { return { results: [], errors: [], metrics: createParallelMetrics() }; } const concurrency = options?.concurrency ?? getMaxConcurrency(); const rateLimiter = getRateLimiter(); const metricsTracker = new ParallelMetricsTracker(operations.length, concurrency); metricsTracker.start(); const results = []; const errors = []; const totalOps = operations.length; const hasProgressCallback = !!options?.onProgress; const hasErrorCallback = !!options?.onError; if (concurrency >= totalOps) { const settled = await Promise.all( operations.map(async (operation, index) => { const operationStart = Date.now(); try { if (rateLimiter) await rateLimiter.acquire(); const value = await operation(); metricsTracker.recordOperation(Date.now() - operationStart); return { success: true, value }; } catch (error) { const err = error; if (hasErrorCallback) options.onError(err, index); return { success: false, error: err, index }; } }) ); for (const result of settled) { if (result.success) { results.push(result.value); } else { errors.push({ index: result.index, error: result.error }); } } if (hasProgressCallback) options.onProgress(totalOps, totalOps); } else { let completedCount = 0; for (let i = 0; i < totalOps; i += concurrency) { const chunkEnd = Math.min(i + concurrency, totalOps); const chunkResults = await Promise.all( operations.slice(i, chunkEnd).map(async (operation, offset) => { const index = i + offset; const operationStart = Date.now(); try { if (rateLimiter) await rateLimiter.acquire(); const value = await operation(); metricsTracker.recordOperation(Date.now() - operationStart); return { success: true, value }; } catch (error) { const err = error; if (hasErrorCallback) options.onError(err, index); return { success: false, error: err, index }; } }) ); for (const result of chunkResults) { if (result.success) { results.push(result.value); } else { errors.push({ index: result.index, error: result.error }); } } if (hasProgressCallback) { completedCount = chunkEnd; options.onProgress(completedCount, totalOps); } } } const metrics = metricsTracker.complete(); return { results, errors, metrics }; } function chunkForParallel(items, batchSize, concurrency) { if (items.length === 0) return []; if (batchSize <= 0) throw new Error("batchSize must be positive"); if (concurrency <= 0) throw new Error("concurrency must be positive"); const chunks = []; for (let i = 0; i < items.length; i += batchSize) { chunks.push(items.slice(i, i + batchSize)); } return chunks; } function getOptimalConcurrency(operationType, itemCount) { const maxConcurrency = getMaxConcurrency(); if (itemCount < 100) return 1; if (itemCount < 1e3) { return Math.min(2, maxConcurrency); } if (itemCount < 1e4) { return Math.min(4, maxConcurrency); } return Math.min(8, maxConcurrency); } function shouldUseParallel(itemCount, poolSize) { if (itemCount < 100) { console.warn("\u26A0\uFE0F Dataset too small for parallel execution benefit. Using sequential."); return false; } if (poolSize === 1) { console.info("\u2139\uFE0F Connection pool size is 1. Using sequential execution."); return false; } return true; } // src/types/search.types.ts exports.FindByFilterOptions = void 0; ((FindByFilterOptions2) => { FindByFilterOptions2.defaultOptions = { onlyOne: false, relationsToInclude: [], search: void 0, pagination: void 0, orderBy: void 0, parallel: void 0, concurrency: void 0, rateLimit: void 0 }; })(exports.FindByFilterOptions || (exports.FindByFilterOptions = {})); // src/data-utils.ts var DataUtils = class { /** * Processes relational data by transforming nested objects and arrays into Prisma-compatible formats. * Converts objects into `connect` or `create` structures for relational integrity. * JSON fields and scalar arrays are preserved as-is without wrapping in connect/create. * @param data The original data object containing relations. * @param modelInfo Optional model information to detect JSON fields and scalar arrays * @returns A transformed object formatted for Prisma operations. */ static processRelations(data, modelInfo) { const processedData = { ...data }; const jsonFields = /* @__PURE__ */ new Set(); const scalarArrayFields = /* @__PURE__ */ new Set(); if (modelInfo?.fields) { for (const field of modelInfo.fields) { if (field.kind === "scalar" && (field.type === "Json" || field.type === "Bytes")) { jsonFields.add(field.name); } if (field.kind === "scalar" && field.isList === true) { scalarArrayFields.add(field.name); } } } for (const key of Object.keys(data)) { const value = data[key]; if (!this.isObject(value)) continue; if (jsonFields.has(key)) { processedData[key] = value; continue; } if (scalarArrayFields.has(key)) { processedData[key] = value; continue; } if (Array.isArray(value)) { const relationArray = this.processRelationArray(value); if (relationArray.length > 0) { processedData[key] = { connect: relationArray }; } } else { processedData[key] = this.processRelationObject(value); } } return processedData; } static isObject(val) { return typeof val === "object" && val !== null; } static processRelationArray(array) { return array.map((item) => item?.id !== void 0 ? { id: item.id } : null).filter(Boolean); } static processRelationObject(obj) { if (obj?.id !== void 0) { return { connect: { id: obj.id } }; } return { create: { ...obj } }; } static normalizeRelationsToFK(data, keyTransformTemplate = (key) => `${key}Id`) { const flatData = { ...data }; for (const [key, value] of Object.entries(flatData)) { if (typeof value === "object" && value !== null && "connect" in value && value.connect && typeof value.connect === "object" && "id" in value.connect) { const newKey = keyTransformTemplate(key); if (!(newKey in flatData)) { flatData[newKey] = value.connect.id; } delete flatData[key]; } } return flatData; } }; // src/model-utils.ts init_config(); var ModelUtils = class { static { this.MAX_DEPTH = 3; } /** * Gets the dependency tree for models based on their relationships * Returns models in topological order (dependencies first) */ static getModelDependencyTree(modelNames) { const prisma = getPrismaInstance(); const runtimeDataModel = prisma._runtimeDataModel; const modelDeps = []; for (const modelName of modelNames) { const modelMeta = runtimeDataModel.models[modelName]; if (!modelMeta) { throw new Error(`Model "${modelName}" not found in runtime data model.`); } const dependencies = []; const relationFields = Object.values(modelMeta.fields).filter( (field) => field.kind === "object" && field.relationName && !field.isList ); for (const field of relationFields) { const relatedModel = field.type; if (modelNames.includes(relatedModel) && relatedModel !== modelName) { dependencies.push(relatedModel); } } modelDeps.push({ name: modelName, dependencies }); } return modelDeps; } /** * Sorts models in topological order based on their dependencies */ static sortModelsByDependencies(models) { const visited = /* @__PURE__ */ new Set(); const sorted = []; function visit(modelName, visiting = /* @__PURE__ */ new Set()) { if (visited.has(modelName)) return; if (visiting.has(modelName)) { throw new Error(`Circular dependency detected involving model: ${modelName}`); } visiting.add(modelName); const model = models.find((m) => m.name === modelName); if (model) { for (const dep of model.dependencies) { visit(dep, visiting); } } visiting.delete(modelName); visited.add(modelName); sorted.push(modelName); } for (const model of models) { visit(model.name); } return sorted; } /** * Finds the path from a child model to a parent model through relationships */ static findPathToParentModel(fromModel, toModel, maxDepth = 5) { const prisma = getPrismaInstance(); const runtimeDataModel = prisma._runtimeDataModel; if (!runtimeDataModel?.models[fromModel]) { throw new Error(`Model "${fromModel}" not found in runtime data model.`); } if (!runtimeDataModel?.models[toModel]) { throw new Error(`Model "${toModel}" not found in runtime data model.`); } const queue = [ { model: fromModel, path: [] } ]; const visited = /* @__PURE__ */ new Set(); while (queue.length > 0) { const current = queue.shift(); if (current.path.length >= maxDepth) continue; if (visited.has(current.model)) continue; visited.add(current.model); const modelMeta = runtimeDataModel.models[current.model]; if (!modelMeta) continue; const relationFields = Object.values(modelMeta.fields).filter( (field) => field.kind === "object" && field.relationName && !field.isList ).map((field) => ({ name: field.name, type: field.type })); for (const field of relationFields) { const newPath = [...current.path, field.name]; if (field.type === toModel) { return newPath.join("."); } queue.push({ model: field.type, path: newPath }); } } return null; } /** * Builds a nested filter object to search by a field in a parent model */ static buildNestedFilterToParent(fromModel, toModel, fieldName, value) { const path = this.findPathToParentModel(fromModel, toModel); if (!path) { const directField = toModel.toLowerCase() + "Id"; return { [directField]: value }; } const pathParts = path.split("."); const filter = {}; let current = filter; for (let i = 0; i < pathParts.length; i++) { const part = pathParts[i]; if (i === pathParts.length - 1) { current[part] = { [fieldName]: value }; } else { current[part] = {}; current = current[part]; } } return filter; } /** * Builds include tree for nested relations based on provided configuration */ static async getIncludesTree(modelName, relationsToInclude = [], currentDepth = 0) { const prisma = getPrismaInstance(); const runtimeDataModel = prisma._runtimeDataModel; const getRelationalFields = (model) => { const modelMeta = runtimeDataModel.models[model]; if (!modelMeta) throw new Error(`Model "${model}" not found in runtime data model.`); return Object.values(modelMeta.fields).filter((field) => field.kind === "object" && field.relationName).map((field) => ({ name: field.name, type: field.type })); }; const isValidField = (fields, name) => fields.find((f) => f.name === name); const buildSubInclude = async (type, subTree, depth) => { if (depth >= this.MAX_DEPTH) { return true; } const subInclude = await this.getIncludesTree(type, subTree, depth + 1); return Object.keys(subInclude).length > 0 ? { include: subInclude } : true; }; const buildInclude = async (model, tree, depth) => { const include = {}; const fields = getRelationalFields(model); const processField = async (name, subTree) => { const field = isValidField(fields, name); if (!field) return; include[name] = await buildSubInclude(field.type, subTree, depth); }; if (tree === "*") { for (const field of fields) { include[field.name] = true; } } else if (Array.isArray(tree)) { for (const node of tree) { for (const [relation, subTree] of Object.entries(node)) { await processField(relation, subTree); } } } return include; }; return await buildInclude(modelName, relationsToInclude, currentDepth); } /** * Gets all model names from Prisma runtime */ static getAllModelNames() { const prisma = getPrismaInstance(); const runtimeDataModel = prisma._runtimeDataModel; return Object.keys(runtimeDataModel.models); } /** * Extracts unique constraints from a model using Prisma runtime * Returns an array of field name arrays that form unique constraints */ static getUniqueConstraints(modelName) { const prisma = getPrismaInstance(); const runtimeDataModel = prisma._runtimeDataModel; const modelMeta = runtimeDataModel?.models[modelName]; if (!modelMeta) { console.warn(`Model "${modelName}" not found in runtime data model.`); return []; } const uniqueConstraints = []; if (modelMeta.uniqueIndexes && Array.isArray(modelMeta.uniqueIndexes)) { for (const index of modelMeta.uniqueIndexes) { if (index.fields && Array.isArray(index.fields)) { uniqueConstraints.push(index.fields); } } } if (modelMeta.fields) { for (const field of Object.values(modelMeta.fields)) { if (field.isUnique && field.name && field.name !== "id") { uniqueConstraints.push([field.name]); } } } if (modelMeta.primaryKey?.fields && Array.isArray(modelMeta.primaryKey.fields) && modelMeta.primaryKey.fields.length > 0) { const pkFields = modelMeta.primaryKey.fields; if (!(pkFields.length === 1 && pkFields[0] === "id")) { uniqueConstraints.push(pkFields); } } return uniqueConstraints; } }; // src/base-entity.ts init_config(); // src/search/condition-utils.ts var ConditionUtils = class { /** * Validates if a value is considered valid for filtering * * @param value - The value to validate * @returns True if the value is valid, false otherwise * * @remarks * - Returns false for: null, undefined, empty strings (including whitespace-only), empty arrays * - Returns false for objects where all nested values are invalid * - Returns true for: non-empty strings, numbers (including 0), booleans, non-empty arrays, valid objects * * @example * ```typescript * ConditionUtils.isValid('hello') // true * ConditionUtils.isValid('') // false * ConditionUtils.isValid(0) // true * ConditionUtils.isValid([]) // false * ConditionUtils.isValid({ key: 'val' }) // true * ``` */ static isValid(value) { if (value === void 0 || value === null) return false; if (typeof value === "string") return value.trim() !== ""; if (typeof value === "boolean") return true; if (typeof value === "number") return true; if (value instanceof Date) return true; if (Array.isArray(value)) return value.length > 0; if (typeof value === "object") { const entries = Object.entries(value); if (entries.length === 0) return false; for (const [, val] of entries) { if (!this.isValid(val)) return false; } return true; } return true; } /** * Creates a Prisma string condition based on the search mode * * @param option - String search options with value and mode * @returns Prisma condition object for string matching * * @remarks * Supported modes: * - LIKE: Creates { contains: value } for substring matching * - STARTS_WITH: Creates { startsWith: value } * - ENDS_WITH: Creates { endsWith: value } * - EXACT (default): Creates { equals: value } for exact matching * * @example * ```typescript * ConditionUtils.string({ value: 'John', mode: 'LIKE' }) * // Returns: { contains: 'John' } * * ConditionUtils.string({ value: 'John', mode: 'STARTS_WITH' }) * // Returns: { startsWith: 'John' } * ``` */ static string(option) { switch (option.mode) { case "LIKE": return { contains: option.value }; case "STARTS_WITH": return { startsWith: option.value }; case "ENDS_WITH": return { endsWith: option.value }; default: return { equals: option.value }; } } /** * Creates a Prisma range condition for numeric or date filtering * * @param option - Range search options with min and/or max values * @returns Prisma condition object with gte/lte operators * * @remarks * - If only min is provided: returns { gte: min } * - If only max is provided: returns { lte: max } * - If both provided: returns { gte: min, lte: max } * - If neither provided: returns empty object * * @example * ```typescript * ConditionUtils.range({ min: 18, max: 65 }) * // Returns: { gte: 18, lte: 65 } * * ConditionUtils.range({ min: new Date('2024-01-01') }) * // Returns: { gte: Date('2024-01-01') } * ``` */ static range(option) { const condition = {}; if (option.min !== void 0) condition.gte = option.min; if (option.max !== void 0) condition.lte = option.max; return condition; } /** * Creates a Prisma list condition for array matching * * @param option - List search options with array of values and mode * @returns Prisma condition object based on mode * * @remarks * Supported modes: * - IN (default): Creates { in: values } for matching any value in the list * - NOT_IN: Creates { notIn: values } for excluding values in the list * - HAS_SOME: Creates { hasSome: values } for array fields that contain some values * - HAS_EVERY: Creates { hasEvery: values } for array fields that contain all values * * @example * ```typescript * ConditionUtils.list({ values: ['active', 'pending'], mode: 'IN' }) * // Returns: { in: ['active', 'pending'] } * * ConditionUtils.list({ values: ['deleted'], mode: 'NOT_IN' }) * // Returns: { notIn: ['deleted'] } * * ConditionUtils.list({ values: ['tag1', 'tag2'], mode: 'HAS_SOME' }) * // Returns: { hasSome: ['tag1', 'tag2'] } * * ConditionUtils.list({ values: ['required1', 'required2'], mode: 'HAS_EVERY' }) * // Returns: { hasEvery: ['required1', 'required2'] } * ``` */ static list(option) { const mode = option.mode || "IN"; switch (mode) { case "NOT_IN": return { notIn: option.values }; case "HAS_SOME": return { hasSome: option.values }; case "HAS_EVERY": return { hasEvery: option.values }; case "IN": default: return { in: option.values }; } } }; // src/search/object-utils.ts var ObjectUtils = class { /** * Assigns a value to a nested path in an object, creating intermediate objects as needed * * @param target - The object to modify * @param path - Dot-separated path to the property (e.g., 'user.profile.name') * @param value - The value to assign * @param modelInfo - Optional Prisma model information for relation-aware structure creation * * @remarks * - Creates intermediate objects if they don't exist * - With modelInfo: wraps new relations with 'is'/'some' based on field type * - Without modelInfo: creates plain nested objects * - Intelligently merges with existing Prisma filter structures (is/some) * - Preserves existing sibling properties * - Handles nested Prisma relation filters correctly * * @example * ```typescript * const obj = {}; * ObjectUtils.assign(obj, 'user.profile.name', 'John'); * // obj is now: { user: { profile: { name: 'John' } } } * * // With existing Prisma filter structure: * const filter = { group: { is: { id: 1 } } }; * ObjectUtils.assign(filter, 'group.course.name', 'Math'); * // filter is now: { group: { is: { id: 1, course: { name: 'Math' } } } } * * // With modelInfo (creates proper Prisma structures): * const filter = {}; * ObjectUtils.assign(filter, 'posts.author.name', { contains: 'John' }, modelInfo); * // filter is now: { posts: { some: { author: { is: { name: { contains: 'John' } } } } } } * ``` */ static assign(target, path, value, modelInfo) { const keys = path.split("."); let current = target; let currentModelInfo = modelInfo; for (let index = 0; index < keys.length; index++) { const key = keys[index]; if (index === keys.length - 1) { current[key] = value; } else if (current[key] && typeof current[key] === "object") { const result = this.navigateIntoExisting(current[key], key, currentModelInfo); current = result.target; currentModelInfo = result.modelInfo; } else { const result = this.createNewStructure(current, key, currentModelInfo); current = result.target; currentModelInfo = result.modelInfo; } } } /** * Navigates into an existing object structure, detecting Prisma wrappers * @private */ static navigateIntoExisting(obj, key, modelInfo) { let target = obj; if ("is" in obj && typeof obj.is === "object") { target = obj.is; } else if ("some" in obj && typeof obj.some === "object") { target = obj.some; } return { target, modelInfo: this.getNextModelInfo(key, modelInfo) }; } /** * Creates a new structure for a key, with Prisma awareness if modelInfo provided * @private */ static createNewStructure(parent, key, modelInfo) { if (!modelInfo) { parent[key] = {}; return { target: parent[key], modelInfo: null }; } const field = modelInfo?.fields?.find((f) => f.name === key); if (field && field.kind === "object") { const wrapper = field.isList ? "some" : "is"; parent[key] = { [wrapper]: {} }; return { target: parent[key][wrapper], modelInfo: this.getNextModelInfo(key, modelInfo) }; } parent[key] = {}; return { target: parent[key], modelInfo: null }; } /** * Builds a nested object from a path and value * * @param path - Dot-separated path (e.g., 'user.profile.name') * @param value - The value to place at the end of the path * @returns A nested object with the value at the specified path * * @remarks * Constructs the object from the inside out, starting with the value * and wrapping it in nested objects according to the path * * @example * ```typescript * ObjectUtils.build('user.profile.name', 'John') * // Returns: { user: { profile: { name: 'John' } } } * * ObjectUtils.build('status', 'active') * // Returns: { status: 'active' } * ``` */ static build(path, value) { return path.split(".").reverse().reduce((acc, key) => ({ [key]: acc }), value); } /** * Builds a nested object from a path and value with Prisma relation awareness * Wraps array relations with 'some' and single relations with 'is' * * @param path - Dot-separated path (e.g., 'group.groupMembers.user.idNumber') * @param value - The value to place at the end of the path * @param modelInfo - Optional Prisma model information for relation detection * @returns A nested object with proper Prisma filter wrappers * * @remarks * - Without modelInfo, behaves like build() (no wrappers) * - With modelInfo, detects relation types for each path segment * - Array relations (list: true) are wrapped with { some: {...} } * - Single relations are wrapped with { is: {...} } * - Final field (not a relation) is assigned the value directly * - Uses getPrismaInstance to resolve nested model info * * @example * ```typescript * // Without modelInfo (no wrappers): * ObjectUtils.buildWithRelations('group.name', { contains: 'A' }) * // Returns: { group: { name: { contains: 'A' } } } * * // With modelInfo (array relation 'groupMembers' + single relation 'user'): * ObjectUtils.buildWithRelations('group.groupMembers.user.idNumber', { endsWith: '123' }, modelInfo) * // Returns: { group: { groupMembers: { some: { user: { is: { idNumber: { endsWith: '123' } } } } } } } * ``` */ static buildWithRelations(path, value, modelInfo) { if (!modelInfo) { return this.build(path, value); } const keys = path.split("."); const modelInfoMap = { 0: modelInfo }; let currentModelInfo = modelInfo; for (let i = 0; i < keys.length - 1; i++) { const key = keys[i]; const nextModelInfo = this.getNextModelInfo(key, currentModelInfo); modelInfoMap[i + 1] = nextModelInfo; currentModelInfo = nextModelInfo; } let result = value; for (let i = keys.length - 1; i >= 0; i--) { const key = keys[i]; const fieldModelInfo = modelInfoMap[i]; result = this.wrapFieldWithRelation(key, result, fieldModelInfo); } return result; } /** * Wraps a field value with Prisma relation operators if it's a relation * * @param key - Field name * @param value - Value to wrap * @param modelInfo - Current model information * @returns Wrapped value or plain object * @private */ static wrapFieldWithRelation(key, value, modelInfo) { const field = modelInfo?.fields?.find((f) => f.name === key); if (!field || field.kind !== "object") { return { [key]: value }; } return field.isList ? { [key]: { some: value } } : { [key]: { is: value } }; } /** * Gets the model info for a related model * * @param fieldName - Name of the relation field * @param modelInfo - Current model information * @returns Model info for the related model or null * @private */ static getNextModelInfo(fieldName, modelInfo) { if (!modelInfo?.fields) return null; const field = modelInfo.fields.find((f) => f.name === fieldName); if (!field || field.kind !== "object") return null; try { const { getPrismaInstance: getPrismaInstance2 } = (init_config(), __toCommonJS(config_exports)); const prisma = getPrismaInstance2(); const runtimeDataModel = prisma._runtimeDataModel; const relatedModelName = field.type; return runtimeDataModel?.models?.[relatedModelName] || null; } catch { return null; } } /** * Gets a value from a nested object using a dot-separated path * * @param obj - The object to read from * @param path - Dot-separated path to the property (e.g., 'user.profile.name') * @returns The value at the specified path, or undefined if not found * * @remarks * - Returns undefined if any part of the path doesn't exist * - Handles null/undefined values gracefully in the path * * @example * ```typescript * const obj = { user: { profile: { name: 'John' } } }; * ObjectUtils.get(obj, 'user.profile.name') // 'John' * ObjectUtils.get(obj, 'user.age') // undefined * ``` */ static get(obj, path) { return path.split(".").reduce((acc, key) => acc?.[key], obj); } /** * Removes properties at specified paths and cleans up empty parent objects * * @param filter - The object to modify * @param paths - Set of dot-separated paths to remove * * @remarks * - Removes the property at each specified path * - Recursively removes parent objects that become empty after deletion * - Leaves non-empty parent objects intact * - Handles non-existent paths gracefully * * @example * ```typescript * const obj = { user: { name: 'John', age: 30 }, status: 'active' }; * ObjectUtils.clean(obj, new Set(['user.age'])); * // obj is now: { user: { name: 'John' }, status: 'active' } * * const obj2 = { user: { name: 'John' } }; * ObjectUtils.clean(obj2, new Set(['user.name'])); * // obj2 is now: {} (empty parent removed) * ``` */ static clean(filter, paths) { for (const fullPath of paths) { const { parent, lastKey } = this.getParentAndKey(filter, fullPath); if (!parent || !(lastKey in parent)) continue; delete parent[lastKey]; this.cleanEmptyAncestors(filter, fullPath); } } /** * Gets the parent object and key for a given path * * @param obj - The object to traverse * @param path - Dot-separated path to navigate * @returns Object containing the parent object and the last key, or undefined parent if path invalid * @private * * @remarks * Used internally by clean() to locate the parent of a property to be deleted */ static getParentAndKey(obj, path) { const keys = path.split("."); const lastKey = keys.at(-1); let current = obj; for (let i = 0; i < keys.length - 1; i++) { const key = keys[i]; if (typeof current[key] !== "object") { return { parent: void 0, lastKey }; } current = current[key]; } return { parent: current, lastKey }; } /** * Recursively removes empty ancestor objects after a property deletion * * @param obj - The root object * @param path - The path where deletion occurred * @private * * @remarks * Walks up the path hierarchy, removing objects that have become empty * Stops when it encounters a non-empty object or reaches the root */ static cleanEmptyAncestors(obj, path) { const keys = path.split("."); for (let depth = keys.length - 1; depth > 0; depth--) { const currentPath = keys.slice(0, depth).join("."); const currentKey = keys[depth - 1]; const currentObj = currentPath.includes(".") ? this.get(obj, currentPath.split(".").slice(0, -1).join(".")) : obj; if (!currentObj) break; const targetObj = currentObj[currentKey]; if (targetObj && typeof targetObj === "object" && !Array.isArray(targetObj) && Object.keys(targetObj).length === 0) { delete currentObj[currentKey]; } else { break; } } } }; // src/search/search-builder.ts var SearchBuilder = class { /** * Builds a complete search filter by applying string, range, and list searches * * @param baseFilter - The base filter object to extend * @param options - Search options containing string, range, and list searches * @param modelInfo - Optional Prisma model information for relation detection * @returns Combined filter object with all search conditions applied * * @example * ```typescript * const filter = SearchBuilder.build( * { isActive: true }, * { * stringSearch: [{ keys: ['name'], value: 'John', mode: 'LIKE' }], * rangeSearch: [{ keys: ['age'], min: 18, max: 65 }] * } * ); * // Returns: { isActive: true, name: { contains: 'John' }, age: { gte: 18, lte: 65 } } * ``` */ static build(baseFilter, options, modelInfo) { const filter = { ...baseFilter }; if (options.stringSearch) this.apply(filter, options.stringSearch, ConditionUtils.string, modelInfo); if (options.rangeSearch) this.apply(filter, options.rangeSearch, ConditionUtils.range, modelInfo); if (options.listSearch) this.apply(filter, options.listSearch, ConditionUtils.list, modelInfo); return filter; } /** * Applies a specific type of search condition to the filter * Handles both AND and OR grouping of conditions * * @template T - Type of search condition with optional keys and grouping * @param filter - The filter object to modify * @param conditions - Array of search conditions to apply * @param buildCondition - Function to build the condition object from the search option * @param modelInfo - Optional Prisma model information for relation detection * @private * * @remarks * - Skips invalid conditions using ConditionUtils.isValid() * - For OR grouping: adds conditions to filter.OR array and tracks paths for cleanup * - For AND grouping: assigns conditions directly to filter using ObjectUtils.assign() * - Cleans up duplicate paths that appear in OR conditions */ static apply(filter, conditions, buildCondition, modelInfo) { const orPaths = /* @__PURE__ */ new Set(); for (const option of conditions) { const keys = option.keys ?? []; const grouping = option.grouping ?? "and"; const condition = buildCondition(option); if (!ConditionUtils.isValid(condition)) continue; if (grouping === "or") { filter.OR = filter.OR ?? []; for (const path of keys) { filter.OR.push(ObjectUtils.buildWithRelations(path, condition, modelInfo)); orPaths.add(path); } } else { for (const path of keys) { ObjectUtils.assign(filter, path, condition, modelInfo); } } } ObjectUtils.clean(filter, orPaths); } }; // src/search/search-utils.ts init_config(); var SearchUtils = class { /** * Applies search filters to a base filter using SearchBuilder * * @param baseFilter - The base filter object to extend * @param searchOptions - Search options with string, range, and list searches * @param modelInfo - Optional Prisma model information for relation detection * @returns Combined filter with search conditions applied * * @remarks * This is a wrapper around SearchBuilder.build() for convenience * Combines the base filter with advanced search options * * @example * ```typescript * const filter = SearchUtils.applySearchFilter( * { isActive: true }, * { * stringSearch: [{ keys: ['name'], value: 'John', mode: 'LIKE' }] * } * ); * ``` */ static applySearchFilter(baseFilter, searchOptions, modelInfo) { return SearchBuilder.build(baseFilter, searchOptions, modelInfo); } /** * Converts plain filter objects into Prisma-compatible query conditions * Automatically detects field types and applies appropriate conditions * * @param input - Plain object with field values to filter by * @param modelInfo - Optional Prisma model information for relation detection * @returns Prisma-compatible filter object * * @remarks * Automatic condition mapping: * - Strings/Numbers/Dates → { equals: value } * - Arrays → { hasEvery: value } * - Nested objects → { is: {...} } for single relations * - Nested objects → { some: {...} } for array relations (when modelInfo provided) * - Skips null, undefined, empty strings, and empty arrays * * @example * ```typescript * SearchUtils.applyDefaultFilters({ name: 'John', age: 30 }) * // Returns: { name: { equals: 'John' }, age: { equals: 30 } } * * SearchUtils.applyDefaultFilters({ author: { name: 'John' } }) * // Returns: { author: { is: { name: { equals: 'John' } } } } * ``` */ static applyDefaultFilters(input, modelInfo) { const output = {}; for (const [key, value] of Object.entries(input)) { if (!ConditionUtils.isValid(value)) continue; const condition = this.buildDefaultCondition(value, key, modelInfo); if (!condition) continue; ObjectUtils.assign(output, key, condition); } return output; } /** * Builds a default condition based on value type * * @param value - The value to create a condition for * @param fieldName - Optional field name for relation detection * @param modelInfo - Optional model information for relation type detection * @returns Prisma condition object or undefined for invalid values * @private * * @remarks * - Scalars (string/number/boolean/Date) → { equals: value } * - Arrays → { hasEvery: value } (or undefined if empty) * - Objects → { is: {...} } or { some: {...} } depending on relation type */ static buildDefaultCondition(value, fieldName, modelInfo) { if (typeof value === "string" || typeof value === "number" || typeof value === "boolean" || value instanceof Date) { return { equals: value }; } if (Array.isArray(value)) { return value.length > 0 ? { hasEvery: value } : void 0; } if (typeof value === "object" && value !== null) { const nestedModelInfo = fieldName && modelInfo ? this.getRelationModelInfo(fieldName, modelInfo) : void 0; const nested = this.applyDefaultFilters(value, nestedModelInfo); if (!ConditionUtils.isValid(nested)) return void 0; if (fieldName && modelInfo && this.isArrayRelation(fieldName, modelInfo)) { return { some: nested }; } return { is: nested }; } return void 0; } /** * Determines if a field represents an array relation in the Prisma model * * @param fieldName - The name of the field to check * @param modelInfo - Prisma model information containing field definitions * @returns True if the field is an array relation (isList: true), false otherwise * @private * * @remarks * Used to determine whether to use 'some' or 'is' for nested object filters * Array relations use 'some', single relations use 'is' */ static isArrayRelation(fieldName, modelInfo) { if (!modelInfo?.fields) return false; const field = modelInfo.fields.find((f) => f.name === fieldName); if (!field) return false; return field.kind === "object" && field.isList === true; } /** * Gets the model info for a related field * * @param fieldName - The name of the relation field * @param modelInfo - Parent model information * @returns Model info for the related model, or undefined if not found * @private */ static getRelationModelInfo(fieldName, modelInfo) { if (!modelInfo?.fields) return void 0; const field = modelInfo.fields.find((f) => f.name === fieldName); if (!field || field.kind !== "object") return void 0; const relatedModelName = field.type; try { const prisma = getPrismaInstance(); const runtimeDataModel = prisma._runtimeDataModel; if (!runtimeDataModel?.models?.[relatedModelName]) { return void 0; } return runtimeDataModel.models[relatedModelName]; } catch (error) { return void 0; } } /** * Generates string search options for all string fields in a filter object * * @param filters - Object with field values to create search options from * @param mode - Search mode to apply (default: 'EXACT') * @param grouping - Whether to use 'and' or 'or' grouping (default: 'and') * @returns Array of string search options for all non-empty string fields * * @remarks * - Only processes string fields with non-empty values * - Useful for quickly creating search options from form data * - Each field gets its own search option * * @example * ```typescript * SearchUtils.getCustomSearchOptionsForAll( * { name: 'John', email: 'john@example.com' }, * 'LIKE', * 'or' * ); * // Returns: [ * // { keys: ['name'], value: 'John', mode: 'LIKE', grouping: 'or' }, * // { keys: ['email'], value: 'john@example.com', mode: 'LIKE', grouping: 'or' } * // ] * ``` */ static getCustomSearchOptionsForAll(filters, mode = "EXACT", grouping = "and") { return Object.entries(filters).filter(([_, v]) => typeof v === "string" && v.trim() !== "").map(([k, v]) => ({ keys: [