UNPKG

@elsikora/nestjs-crud-automator

Version:

A library for automating the creation of CRUD operations in NestJS.

266 lines (263 loc) 10.1 kB
import { In } from 'typeorm'; /** * Merges two WHERE expressions by building a Cartesian product of OR branches. * @template E - Entity type * @param {TApiAuthorizationScopeWhere<E>} baseWhere - Existing filter. * @param {TApiAuthorizationScopeWhere<E>} scopedWhere - Additional scope filter. * @returns {TApiAuthorizationScopeWhere<E>} Combined filter. */ function AuthorizationScopeMergeWhere(baseWhere, scopedWhere) { if (!baseWhere) { return scopedWhere; } if (!scopedWhere) { return baseWhere; } const baseVariants = Array.isArray(baseWhere) ? baseWhere : [baseWhere]; const scopedVariants = Array.isArray(scopedWhere) ? scopedWhere : [scopedWhere]; const mergedVariants = []; for (const baseVariant of baseVariants) { for (const scopedVariant of scopedVariants) { mergedVariants.push(mergeRecordValues(baseVariant, scopedVariant)); } } return mergedVariants.length === 1 ? mergedVariants[0] : mergedVariants; } /** * Compares two where-clause leaf values for semantic equality. * @param {unknown} left - Existing value. * @param {unknown} right - Scoped value. * @returns {boolean} True when both values represent the same condition. */ function areValuesEquivalent(left, right) { if (isFindOperator(left) && isFindOperator(right)) { return left._type === right._type && areValuesEquivalent(left._value, right._value); } if (Array.isArray(left) && Array.isArray(right)) { if (left.length !== right.length) { return false; } return left.every((value, index) => areValuesEquivalent(value, right[index])); } if (isRecord(left) && isRecord(right)) { const leftKeys = Object.keys(left).toSorted((a, b) => a.localeCompare(b)); const rightKeys = Object.keys(right).toSorted((a, b) => a.localeCompare(b)); if (!areValuesEquivalent(leftKeys, rightKeys)) { return false; } return leftKeys.every((key) => areValuesEquivalent(left[key], right[key])); } return left === right; } /** * Compares two scalar-like values for ordering-sensitive operators. * @param {unknown} left - Current value. * @param {unknown} right - Operator boundary value. * @returns {number} Comparison result suitable for inequality operators. */ function compareValues(left, right) { const leftDate = normalizeDateValue(left); const rightDate = normalizeDateValue(right); if (leftDate !== undefined && rightDate !== undefined) { return leftDate - rightDate; } const leftNumber = normalizeNumberValue(left); const rightNumber = normalizeNumberValue(right); if (leftNumber !== undefined && rightNumber !== undefined) { return leftNumber - rightNumber; } return toComparableText(left).localeCompare(toComparableText(right)); } /** * Builds a match-nothing FindOperator branch for impossible conflicts. * @returns {ITypeormFindOperatorShape} TypeORM `In([])` operator. */ function createMatchNothingOperator() { return In([]); } /** * Detects TypeORM FindOperator-like values without depending on private typings. * @param {unknown} value - Candidate value. * @returns {boolean} True when the value looks like a FindOperator. */ function isFindOperator(value) { return Boolean(value && typeof value === "object" && "_type" in value && "_value" in value); } /** * Detects plain record objects that can be merged recursively. * @param {unknown} value - Candidate value. * @returns {boolean} True when the value is a mergeable record. */ function isRecord(value) { return Boolean(value) && typeof value === "object" && !Array.isArray(value) && !isFindOperator(value); } /** * Checks whether a concrete scalar satisfies a TypeORM FindOperator condition. * @param {unknown} value - Concrete value. * @param {ITypeormFindOperatorShape} operator - FindOperator to evaluate. * @returns {boolean} True when the scalar satisfies the operator. */ function matchesFindOperator(value, operator) { switch (operator._type) { case "between": { const boundaries = Array.isArray(operator._value) ? operator._value : [undefined, undefined]; const start = boundaries[0]; const end = boundaries[1]; return compareValues(value, start) >= 0 && compareValues(value, end) <= 0; } case "equal": { return areValuesEquivalent(value, operator._value); } case "ilike": { return matchesLike(value, operator._value, true); } case "in": { const values = Array.isArray(operator._value) ? operator._value : [operator._value]; return values.some((entry) => (isFindOperator(entry) ? matchesFindOperator(value, entry) : areValuesEquivalent(value, entry))); } case "isnull": { return value === null || value === undefined; } case "lessThan": { return compareValues(value, operator._value) < 0; } case "lessThanOrEqual": { return compareValues(value, operator._value) <= 0; } case "like": { return matchesLike(value, operator._value, false); } case "moreThan": { return compareValues(value, operator._value) > 0; } case "moreThanOrEqual": { return compareValues(value, operator._value) >= 0; } case "not": { return isFindOperator(operator._value) ? !matchesFindOperator(value, operator._value) : !areValuesEquivalent(value, operator._value); } default: { return false; } } } /** * Evaluates SQL-like wildcard matching against a concrete scalar. * @param {unknown} value - Concrete value. * @param {unknown} pattern - SQL-like pattern. * @param {boolean} isCaseInsensitive - Whether matching should ignore case. * @returns {boolean} True when the value matches the pattern. */ function matchesLike(value, pattern, isCaseInsensitive) { if (value === null || value === undefined) { return false; } const text = toComparableText(value); const rawPattern = toComparableText(pattern); const needle = rawPattern.replaceAll("%", ""); if (needle.length === 0) { return true; } if (isCaseInsensitive) { return text.toLowerCase().includes(needle.toLowerCase()); } return text.includes(needle); } /** * Recursively merges nested record values using logical AND semantics. * @param {Record<string, unknown>} baseValue - Existing branch. * @param {Record<string, unknown>} scopedValue - Scoped branch. * @returns {Record<string, unknown>} Merged branch. */ function mergeRecordValues(baseValue, scopedValue) { const mergedValue = { ...baseValue }; const keys = new Set([...Object.keys(baseValue), ...Object.keys(scopedValue)]); for (const key of keys) { const currentBaseValue = baseValue[key]; const currentScopedValue = scopedValue[key]; if (currentBaseValue === undefined) { mergedValue[key] = currentScopedValue; continue; } if (currentScopedValue === undefined) { mergedValue[key] = currentBaseValue; continue; } mergedValue[key] = mergeWhereValue(currentBaseValue, currentScopedValue); } return mergedValue; } /** * Merges two where-clause leaf values without allowing overwrite semantics. * @param {unknown} baseValue - Existing value. * @param {unknown} scopedValue - Scoped value. * @returns {unknown} Narrowed value or a match-nothing operator on conflict. */ function mergeWhereValue(baseValue, scopedValue) { if (isRecord(baseValue) && isRecord(scopedValue)) { return mergeRecordValues(baseValue, scopedValue); } if (areValuesEquivalent(baseValue, scopedValue)) { return baseValue; } if (isFindOperator(baseValue) && !isFindOperator(scopedValue)) { return matchesFindOperator(scopedValue, baseValue) ? scopedValue : createMatchNothingOperator(); } if (!isFindOperator(baseValue) && isFindOperator(scopedValue)) { return matchesFindOperator(baseValue, scopedValue) ? baseValue : createMatchNothingOperator(); } return createMatchNothingOperator(); } /** * Normalizes date-like values into comparable timestamps. * @param {unknown} value - Candidate date-like value. * @returns {number | undefined} Timestamp when the value is date-like. */ function normalizeDateValue(value) { if (value instanceof Date) { return value.getTime(); } if (typeof value === "string" || typeof value === "number") { const date = new Date(value); const timestamp = date.getTime(); return Number.isNaN(timestamp) ? undefined : timestamp; } return undefined; } /** * Normalizes numeric values and numeric strings into numbers. * @param {unknown} value - Candidate numeric value. * @returns {number | undefined} Numeric representation when available. */ function normalizeNumberValue(value) { if (typeof value === "number") { return value; } if (typeof value === "bigint") { return Number(value); } if (typeof value === "string" && value.trim().length > 0) { const parsedValue = Number(value); return Number.isNaN(parsedValue) ? undefined : parsedValue; } return undefined; } /** * Converts scalar-like values into safe comparable text. * @param {unknown} value - Candidate scalar. * @returns {string} Text representation without object default stringification. */ function toComparableText(value) { if (typeof value === "string") { return value; } if (typeof value === "number" || typeof value === "bigint" || typeof value === "boolean") { return value.toString(); } if (value instanceof Date) { return value.toISOString(); } return ""; } export { AuthorizationScopeMergeWhere }; //# sourceMappingURL=scope-merge-where.utility.js.map