@elsikora/nestjs-crud-automator
Version:
A library for automating the creation of CRUD operations in NestJS.
266 lines (263 loc) • 10.1 kB
JavaScript
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